fixed merge conflicts

This commit is contained in:
bill bittner 2018-02-27 15:30:20 -08:00
commit 6bc89527ad
115 changed files with 2422 additions and 27317 deletions

View file

@ -1,9 +1,10 @@
{
"extends": "standard",
"extends": ["standard", "standard-jsx"],
"env": {
"es6": true,
"jest": true,
"node": true
"node": true,
"browser": true
},
"globals": {
"GENTLY": true

5
.gitignore vendored
View file

@ -1,4 +1,7 @@
node_modules
.idea
config/sequelizeCliConfig.js
config/speechConfig.js
config/speechConfig.js
public/bundle
server.js
webpack.config.js

15
LICENSE Normal file
View file

@ -0,0 +1,15 @@
The MIT License (MIT)
Copyright (c) 2017-2018 LBRY Inc
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

View file

@ -1,5 +1,5 @@
# spee.ch
spee.ch is a single-serving site that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
# Spee.ch
Spee.ch is a web app that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
## how to run this repository locally
* start mysql
@ -16,9 +16,9 @@ spee.ch is a single-serving site that reads and publishes images and videos to a
* run `npm install`
* create your `speechConfig.js` file
* copy `speechConfig.js.example` and name it `speechConfig.js`
* replace the `null` values in the config file with the appropriate values for your environement
* to start the server, from your command line run `node speech.js`
* To run hot, use `nodemon` instead of `node`
* replace the `null` values in the config file with the appropriate values for your environment
* build the app by running `npm run build-prod`
* to start the server, run `npm run start`
* visit [localhost:3000](http://localhost:3000)
## Tests
@ -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/: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-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

@ -7,31 +7,31 @@ module.exports = {
const userName = channelName.substring(1);
logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`);
db.User
.findOne({where: { userName }})
.then(user => {
if (!user) {
logger.debug('no user found');
.findOne({where: { userName }})
.then(user => {
if (!user) {
logger.debug('no user found');
resolve(false);
return;
}
return user.comparePassword(userPassword, (passwordErr, isMatch) => {
if (passwordErr) {
logger.error('comparePassword error:', passwordErr);
resolve(false);
return;
}
return user.comparePassword(userPassword, (passwordErr, isMatch) => {
if (passwordErr) {
logger.error('comparePassword error:', passwordErr);
resolve(false);
return;
}
if (!isMatch) {
logger.debug('incorrect password');
resolve(false);
return;
}
logger.debug('...password was a match...');
resolve(true);
});
})
.catch(error => {
reject(error);
if (!isMatch) {
logger.debug('incorrect password');
resolve(false);
return;
}
logger.debug('...password was a match...');
resolve(true);
});
})
.catch(error => {
reject(error);
});
});
},
authenticateIfNoUserToken (channelName, channelPassword, user) {

View file

@ -24,11 +24,13 @@ module.exports = {
},
site: {
title: 'Spee.ch',
name : 'Spee.ch',
host : 'https://spee.ch',
description: 'Open-source, decentralized image and video sharing.'
},
publish: {
thumbnailChannel: '@channelName:channelId', // create a channel to use for thumbnail images
},
}
claim: {
defaultTitle : 'Spee.ch',
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',

View file

@ -10,80 +10,80 @@ module.exports = {
let publishResults, certificateId, channelName;
// publish the file
return lbryApi.publishClaim(publishParams)
.then(tx => {
logger.info(`Successfully published ${publishParams.name} ${fileName}`, tx);
publishResults = tx;
// get the channel information
if (publishParams.channel_name) {
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
} else {
logger.debug('this claim was not published in a channel');
return null;
}
})
.then(channel => {
.then(tx => {
logger.info(`Successfully published ${publishParams.name} ${fileName}`, tx);
publishResults = tx;
// get the channel information
if (publishParams.channel_name) {
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
} else {
logger.debug('this claim was not published in a channel');
return null;
}
})
.then(channel => {
// set channel information
certificateId = null;
channelName = null;
if (channel) {
certificateId = channel.channelClaimId;
channelName = channel.channelName;
}
logger.debug(`certificateId: ${certificateId}`);
})
.then(() => {
certificateId = null;
channelName = null;
if (channel) {
certificateId = channel.channelClaimId;
channelName = channel.channelName;
}
logger.debug(`certificateId: ${certificateId}`);
})
.then(() => {
// create the File record
const fileRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
description: publishParams.metadata.description,
address : publishParams.claim_address,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
fileName,
filePath : publishParams.file_path,
fileType,
nsfw : publishParams.metadata.nsfw,
};
// create the Claim record
const claimRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
description: publishParams.metadata.description,
address : publishParams.claim_address,
thumbnail : publishParams.metadata.thumbnail,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
contentType: fileType,
nsfw : publishParams.metadata.nsfw,
amount : publishParams.bid,
certificateId,
channelName,
};
// upsert criteria
const upsertCriteria = {
name : publishParams.name,
claimId: publishResults.claim_id,
};
// upsert the records
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim')]);
})
.then(([file, claim]) => {
logger.debug('File and Claim records successfully created');
return Promise.all([file.setClaim(claim), claim.setFile(file)]);
})
.then(() => {
logger.debug('File and Claim records successfully associated');
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
})
.catch(error => {
logger.error('PUBLISH ERROR', error);
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
reject(error);
});
const fileRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
description: publishParams.metadata.description,
address : publishParams.claim_address,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
fileName,
filePath : publishParams.file_path,
fileType,
nsfw : publishParams.metadata.nsfw,
};
// create the Claim record
const claimRecord = {
name : publishParams.name,
claimId : publishResults.claim_id,
title : publishParams.metadata.title,
description: publishParams.metadata.description,
address : publishParams.claim_address,
thumbnail : publishParams.metadata.thumbnail,
outpoint : `${publishResults.txid}:${publishResults.nout}`,
height : 0,
contentType: fileType,
nsfw : publishParams.metadata.nsfw,
amount : publishParams.bid,
certificateId,
channelName,
};
// upsert criteria
const upsertCriteria = {
name : publishParams.name,
claimId: publishResults.claim_id,
};
// upsert the records
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim')]);
})
.then(([file, claim]) => {
logger.debug('File and Claim records successfully created');
return Promise.all([file.setClaim(claim), claim.setFile(file)]);
})
.then(() => {
logger.debug('File and Claim records successfully associated');
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
})
.catch(error => {
logger.error('PUBLISH ERROR', error);
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
reject(error);
});
});
},
checkClaimNameAvailability (name) {

View file

@ -6,11 +6,11 @@ module.exports = function () {
for (let configCategoryKey in config) {
if (config.hasOwnProperty(configCategoryKey)) {
// get the final variables for each config category
// get the final variables for each config category
const configVariables = config[configCategoryKey];
for (let configVarKey in configVariables) {
if (configVariables.hasOwnProperty(configVarKey)) {
// print each variable
// print each variable
logger.debug(`CONFIG CHECK: ${configCategoryKey}.${configVarKey} === ${configVariables[configVarKey]}`);
}
}

View file

@ -1,37 +1,30 @@
const logger = require('winston');
module.exports = {
returnErrorMessageAndStatus: function (error) {
let status, message;
// check for daemon being turned off
if (error.code === 'ECONNREFUSED') {
status = 200;
message = 'Connection refused. The daemon may not be running.';
// check for thrown errors
} else if (error.message) {
status = 200;
message = error.message;
// fallback for everything else
} else {
status = 400;
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));
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.';
// fallback for everything else
} else {
status = 400;
if (error.message) {
message = error.message;
} else {
message = error;
};
};
return [status, message];
},
useObjectPropertiesIfNoKeys: function (err) {
if (Object.keys(err).length === 0) {
let newErrorObject = {};

View file

@ -0,0 +1,62 @@
const logger = require('winston');
const ua = require('universal-analytics');
const config = require('../config/speechConfig.js');
const googleApiKey = config.analytics.googleId;
function createServeEventParams (headers, ip, originalUrl) {
return {
eventCategory : 'client requests',
eventAction : 'serve request',
eventLabel : originalUrl,
ipOverride : ip,
userAgentOverride: headers['user-agent'],
};
};
function createPublishTimingEventParams (label, startTime, endTime, ip, headers) {
const durration = endTime - startTime;
return {
userTimingCategory : 'lbrynet',
userTimingVariableName: 'publish',
userTimingTime : durration,
userTimingLabel : label,
uip : ip,
userAgentOverride : headers['user-agent'],
};
};
function sendGoogleAnalyticsEvent (ip, params) {
const visitorId = ip.replace(/\./g, '-');
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
visitor.event(params, (err) => {
if (err) {
logger.error('Google Analytics Event Error >>', err);
}
});
};
function sendGoogleAnalyticsTiming (ip, params) {
const visitorId = ip.replace(/\./g, '-');
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
visitor.timing(params, (err) => {
if (err) {
logger.error('Google Analytics Event Error >>', err);
}
logger.debug(`Timing event successfully sent to google analytics`);
});
};
module.exports = {
sendGAServeEvent (headers, ip, originalUrl) {
const params = createServeEventParams(headers, ip, originalUrl);
sendGoogleAnalyticsEvent(ip, params);
},
sendGAAnonymousPublishTiming (headers, ip, originalUrl, startTime, endTime) {
const params = createPublishTimingEventParams('PUBLISH_ANONYMOUS_CLAIM', startTime, endTime, ip, headers);
sendGoogleAnalyticsTiming(ip, params);
},
sendGAChannelPublishTiming (headers, ip, originalUrl, startTime, endTime) {
const params = createPublishTimingEventParams('PUBLISH_IN_CHANNEL_CLAIM', startTime, endTime, ip, headers);
sendGoogleAnalyticsTiming(ip, params);
},
};

View file

@ -0,0 +1,45 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { createStore } from 'redux';
import Reducer from '../react/reducers';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import GAListener from '../react/components/GAListener';
import App from '../react/app';
import renderFullPage from './renderFullPage.js';
import Helmet from 'react-helmet';
module.exports = (req, res) => {
let context = {};
// create a new Redux store instance
const store = createStore(Reducer);
// render component to a string
const html = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<GAListener>
<App />
</GAListener>
</StaticRouter>
</Provider>
);
// get head tags from helmet
const helmet = Helmet.renderStatic();
// check for a redirect
if (context.url) {
// Somewhere a `<Redirect>` was rendered
return res.redirect(301, context.url);
} else {
// we're good, send the response
}
// get the initial state from our Redux store
const preloadedState = store.getState();
// send the rendered page back to the client
res.send(renderFullPage(helmet, html, preloadedState));
};

View file

@ -0,0 +1,71 @@
import React from 'react';
import { renderToString } from 'react-dom/server';
import { createStore, applyMiddleware } from 'redux';
import Reducer from '../react/reducers';
import { Provider } from 'react-redux';
import { StaticRouter } from 'react-router-dom';
import GAListener from '../react/components/GAListener';
import App from '../react/app';
import renderFullPage from './renderFullPage';
import createSagaMiddleware from 'redux-saga';
import { call } from 'redux-saga/effects';
import { handleShowPageUri } from '../react/sagas/show_uri';
import { onHandleShowPageUri } from '../react/actions/show';
import Helmet from 'react-helmet';
const returnSagaWithParams = (saga, params) => {
return function * () {
yield call(saga, params);
};
};
module.exports = (req, res) => {
let context = {};
// create and apply middleware
const sagaMiddleware = createSagaMiddleware();
const middleware = applyMiddleware(sagaMiddleware);
// create a new Redux store instance
const store = createStore(Reducer, middleware);
// create saga
const action = onHandleShowPageUri(req.params);
const saga = returnSagaWithParams(handleShowPageUri, action);
// run the saga middleware
sagaMiddleware
.run(saga)
.done
.then(() => {
console.log('preload sagas are done');
// render component to a string
const html = renderToString(
<Provider store={store}>
<StaticRouter location={req.url} context={context}>
<GAListener>
<App />
</GAListener>
</StaticRouter>
</Provider>
);
// get head tags from helmet
const helmet = Helmet.renderStatic();
// check for a redirect
if (context.url) {
console.log('REDIRECTING:', context.url);
return res.redirect(301, context.url);
} else {
console.log(`we're good, send the response`);
}
// get the initial state from our Redux store
const preloadedState = store.getState();
// send the rendered page back to the client
res.send(renderFullPage(helmet, html, preloadedState));
});
};

View file

@ -1,147 +0,0 @@
const Handlebars = require('handlebars');
const { site, analytics, claim: claimDefaults } = require('../config/speechConfig.js');
function determineOgTitle (storedTitle, defaultTitle) {
return ifEmptyReturnOther(storedTitle, defaultTitle);
};
function determineOgDescription (storedDescription, defaultDescription) {
const length = 200;
let description = ifEmptyReturnOther(storedDescription, defaultDescription);
if (description.length >= length) {
description = `${description.substring(0, length)}...`;
};
return description;
};
function ifEmptyReturnOther (value, replacement) {
if (value === '') {
return replacement;
}
return value;
}
function determineContentTypeFromFileExtension (fileExtension) {
switch (fileExtension) {
case 'jpeg':
case 'jpg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'mp4':
return 'video/mp4';
default:
return 'image/jpeg';
}
};
function determineOgThumbnailContentType (thumbnail) {
if (thumbnail) {
if (thumbnail.lastIndexOf('.') !== -1) {
return determineContentTypeFromFileExtension(thumbnail.substring(thumbnail.lastIndexOf('.')));
}
}
return '';
}
function createOpenGraphDataFromClaim (claim, defaultTitle, defaultDescription) {
let openGraphData = {};
openGraphData['embedUrl'] = `${site.host}/${claim.claimId}/${claim.name}`;
openGraphData['showUrl'] = `${site.host}/${claim.claimId}/${claim.name}`;
openGraphData['source'] = `${site.host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
openGraphData['directFileUrl'] = `${site.host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
openGraphData['ogTitle'] = determineOgTitle(claim.title, defaultTitle);
openGraphData['ogDescription'] = determineOgDescription(claim.description, defaultDescription);
openGraphData['ogThumbnailContentType'] = determineOgThumbnailContentType(claim.thumbnail);
return openGraphData;
};
module.exports = {
placeCommonHeaderTags () {
const headerBoilerplate = `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>${site.title}</title><link rel="stylesheet" href="/assets/css/reset.css" type="text/css"><link rel="stylesheet" href="/assets/css/general.css" type="text/css"><link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">`;
return new Handlebars.SafeString(headerBoilerplate);
},
googleAnalytics () {
const googleApiKey = analytics.googleId;
const gaCode = `<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
ga('create', '${googleApiKey}', 'auto');
ga('send', 'pageview');</script>`;
return new Handlebars.SafeString(gaCode);
},
addOpenGraph (claim) {
const { ogTitle, ogDescription, showUrl, source, ogThumbnailContentType } = createOpenGraphDataFromClaim(claim, claimDefaults.defaultTitle, claimDefaults.defaultDescription);
const thumbnail = claim.thumbnail;
const contentType = claim.contentType;
const ogTitleTag = `<meta property="og:title" content="${ogTitle}" />`;
const ogUrlTag = `<meta property="og:url" content="${showUrl}" />`;
const ogSiteNameTag = `<meta property="og:site_name" content="${site.title}" />`;
const ogDescriptionTag = `<meta property="og:description" content="${ogDescription}" />`;
const ogImageWidthTag = '<meta property="og:image:width" content="600" />';
const ogImageHeightTag = '<meta property="og:image:height" content="315" />';
const basicTags = `${ogTitleTag} ${ogUrlTag} ${ogSiteNameTag} ${ogDescriptionTag} ${ogImageWidthTag} ${ogImageHeightTag}`;
let ogImageTag = `<meta property="og:image" content="${source}" />`;
let ogImageTypeTag = `<meta property="og:image:type" content="${contentType}" />`;
let ogTypeTag = `<meta property="og:type" content="article" />`;
if (contentType === 'video/mp4') {
const ogVideoTag = `<meta property="og:video" content="${source}" />`;
const ogVideoSecureUrlTag = `<meta property="og:video:secure_url" content="${source}" />`;
const ogVideoTypeTag = `<meta property="og:video:type" content="${contentType}" />`;
ogImageTag = `<meta property="og:image" content="${thumbnail}" />`;
ogImageTypeTag = `<meta property="og:image:type" content="${ogThumbnailContentType}" />`;
ogTypeTag = `<meta property="og:type" content="video" />`;
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag} ${ogVideoTag} ${ogVideoSecureUrlTag} ${ogVideoTypeTag}`);
} else {
if (contentType === 'image/gif') {
ogTypeTag = `<meta property="og:type" content="video.other" />`;
};
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag}`);
}
},
addTwitterCard (claim) {
const { embedUrl, directFileUrl } = createOpenGraphDataFromClaim(claim, claimDefaults.defaultTitle, claimDefaults.defaultDescription);
const basicTwitterTags = `<meta name="twitter:site" content="@spee_ch" >`;
const contentType = claim.contentType;
if (contentType === 'video/mp4') {
const twitterName = '<meta name="twitter:card" content="player" >';
const twitterPlayer = `<meta name="twitter:player" content="${embedUrl}" >`;
const twitterPlayerWidth = '<meta name="twitter:player:width" content="600" >';
const twitterTextPlayerWidth = '<meta name="twitter:text:player_width" content="600" >';
const twitterPlayerHeight = '<meta name="twitter:player:height" content="337" >';
const twitterPlayerStream = `<meta name="twitter:player:stream" content="${directFileUrl}" >`;
const twitterPlayerStreamContentType = '<meta name="twitter:player:stream:content_type" content="video/mp4" >';
return new Handlebars.SafeString(`${basicTwitterTags} ${twitterName} ${twitterPlayer} ${twitterPlayerWidth} ${twitterTextPlayerWidth} ${twitterPlayerHeight} ${twitterPlayerStream} ${twitterPlayerStreamContentType}`);
} else {
const twitterCard = '<meta name="twitter:card" content="summary_large_image" >';
return new Handlebars.SafeString(`${basicTwitterTags} ${twitterCard}`);
}
},
ifConditional (varOne, operator, varTwo, options) {
switch (operator) {
case '===':
return (varOne === varTwo) ? options.fn(this) : options.inverse(this);
case '!==':
return (varOne !== varTwo) ? options.fn(this) : options.inverse(this);
case '<':
return (varOne < varTwo) ? options.fn(this) : options.inverse(this);
case '<=':
return (varOne <= varTwo) ? options.fn(this) : options.inverse(this);
case '>':
return (varOne > varTwo) ? options.fn(this) : options.inverse(this);
case '>=':
return (varOne >= varTwo) ? options.fn(this) : options.inverse(this);
case '&&':
return (varOne && varTwo) ? options.fn(this) : options.inverse(this);
case '||':
return (varOne || varTwo) ? options.fn(this) : options.inverse(this);
case 'mod3':
return ((parseInt(varOne) % 3) === 0) ? options.fn(this) : options.inverse(this);
default:
return options.inverse(this);
}
},
};

View file

@ -1,4 +1,3 @@
const constants = require('../constants');
const logger = require('winston');
const fs = require('fs');
const { site, wallet } = require('../config/speechConfig.js');
@ -177,11 +176,4 @@ module.exports = {
nsfw,
};
},
returnPublishTimingActionType (channelName) {
if (channelName) {
return constants.PUBLISH_IN_CHANNEL_CLAIM;
} else {
return constants.PUBLISH_ANONYMOUS_CLAIM;
}
},
};

32
helpers/renderFullPage.js Normal file
View file

@ -0,0 +1,32 @@
module.exports = (helmet, html, preloadedState) => {
// take the html and preloadedState and return the full page
return `
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<!--helmet-->
${helmet.title.toString()}
${helmet.meta.toString()}
${helmet.link.toString()}
<!--style sheets-->
<link rel="stylesheet" href="/assets/css/reset.css" type="text/css">
<link rel="stylesheet" href="/assets/css/general.css" type="text/css">
<link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">
<!--google font-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
</head>
<body id="main-body">
<div class="row row--tall flex-container--column">
<div id="react-app" class="row row--tall flex-container--column">${html}</div>
</div>
<script>
window.__PRELOADED_STATE__ = ${JSON.stringify(preloadedState).replace(/</g, '\\\u003c')}
</script>
<script src="/bundle/bundle.js"></script>
</body>
</html>
`;
};

View file

@ -1,25 +1,109 @@
const logger = require('winston');
const { getClaimId, getLocalFileRecord } = require('../controllers/serveController.js');
const { handleErrorResponse } = require('../helpers/errorHandlers.js');
const SERVE = 'SERVE';
const SHOW = 'SHOW';
const NO_FILE = 'NO_FILE';
const NO_CHANNEL = 'NO_CHANNEL';
const NO_CLAIM = 'NO_CLAIM';
function clientAcceptsHtml ({accept}) {
return accept && accept.match(/text\/html/);
};
function requestIsFromBrowser (headers) {
return headers['user-agent'] && headers['user-agent'].match(/Mozilla/);
};
function clientWantsAsset ({accept, range}) {
const imageIsWanted = accept && accept.match(/image\/.*/) && !accept.match(/text\/html/) && !accept.match(/text\/\*/);
const videoIsWanted = accept && range;
return imageIsWanted || videoIsWanted;
};
function isValidClaimId (claimId) {
return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));
};
function isValidShortId (claimId) {
return claimId.length === 1; // it should really evaluate the short url itself
};
function isValidShortIdOrClaimId (input) {
return (isValidClaimId(input) || isValidShortId(input));
};
function serveAssetToClient (claimId, name, res) {
return getLocalFileRecord(claimId, name)
.then(fileRecord => {
// check that a local record was found
if (fileRecord === NO_FILE) {
return res.status(307).redirect(`/api/claim/get/${name}/${claimId}`);
}
// serve the file
const {filePath, fileType} = fileRecord;
logger.verbose(`serving file: ${filePath}`);
const sendFileOptions = {
headers: {
'X-Content-Type-Options': 'nosniff',
'Content-Type' : fileType || 'image/jpeg',
},
};
res.status(200).sendFile(filePath, sendFileOptions);
})
.catch(error => {
throw error;
});
};
module.exports = {
serveFile ({ filePath, fileType }, claimId, name, res) {
logger.verbose(`serving file: ${filePath}`);
// set response options
const headerContentType = fileType || 'image/jpeg';
const options = {
headers: {
'X-Content-Type-Options': 'nosniff',
'Content-Type' : headerContentType,
},
};
// send the file
res.status(200).sendFile(filePath, options);
getClaimIdAndServeAsset (channelName, channelClaimId, claimName, claimId, originalUrl, ip, res) {
// get the claim Id and then serve the asset
getClaimId(channelName, channelClaimId, claimName, claimId)
.then(fullClaimId => {
if (fullClaimId === NO_CLAIM) {
return res.status(404).json({success: false, message: 'no claim id could be found'});
} else if (fullClaimId === NO_CHANNEL) {
return res.status(404).json({success: false, message: 'no channel id could be found'});
}
serveAssetToClient(fullClaimId, claimName, res);
// postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
})
.catch(error => {
handleErrorResponse(originalUrl, ip, error, res);
// postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail');
});
},
showFile (claimInfo, shortId, res) {
logger.verbose(`showing claim: ${claimInfo.name}#${claimInfo.claimId}`);
res.status(200).render('index');
determineResponseType (hasFileExtension, headers) {
let responseType;
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 but wants an image/video. Changing response to serve...');
responseType = SERVE;
}
}
return responseType;
},
showFileLite (claimInfo, shortId, res) {
logger.verbose(`showlite claim: ${claimInfo.name}#${claimInfo.claimId}`);
res.status(200).render('index');
flipClaimNameAndIdForBackwardsCompatibility (identifier, name) {
// this is a patch for backwards compatability with '/name/claim_id' url format
if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) {
const tempName = name;
name = identifier;
identifier = tempName;
}
return [identifier, name];
},
logRequestData (responseType, claimName, channelName, claimId) {
logger.debug('responseType ===', responseType);
logger.debug('claim name === ', claimName);
logger.debug('channel name ===', channelName);
logger.debug('claim id ===', claimId);
},
};

View file

@ -1,22 +1,7 @@
const constants = require('../constants');
const logger = require('winston');
const ua = require('universal-analytics');
const config = require('../config/speechConfig.js');
const googleApiKey = config.analytics.googleId;
const db = require('../models');
module.exports = {
createPublishTimingEventParams (publishDurration, ip, headers, label) {
return {
userTimingCategory : 'lbrynet',
userTimingVariableName: 'publish',
userTimingTime : publishDurration,
userTimingLabel : label,
uip : ip,
ua : headers['user-agent'],
ul : headers['accept-language'],
};
},
postToStats (action, url, ipAddress, name, claimId, result) {
logger.debug('action:', action);
// make sure the result is a string
@ -50,46 +35,4 @@ module.exports = {
logger.error('Sequelize error >>', error);
});
},
sendGoogleAnalyticsEvent (action, headers, ip, originalUrl) {
const visitorId = ip.replace(/\./g, '-');
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
let params;
switch (action) {
case 'SERVE':
params = {
ec : 'serve',
ea : originalUrl,
uip: ip,
ua : headers['user-agent'],
ul : headers['accept-language'],
};
break;
default: break;
}
visitor.event(params, (err) => {
if (err) {
logger.error('Google Analytics Event Error >>', err);
}
});
},
sendGoogleAnalyticsTiming (action, headers, ip, originalUrl, startTime, endTime) {
const visitorId = ip.replace(/\./g, '-');
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
const durration = endTime - startTime;
let params;
switch (action) {
case constants.PUBLISH_ANONYMOUS_CLAIM:
case constants.PUBLISH_IN_CHANNEL_CLAIM:
logger.verbose(`${action} completed successfully in ${durration}ms`);
params = module.exports.createPublishTimingEventParams(durration, ip, headers, action);
break;
default: break;
}
visitor.timing(params, (err) => {
if (err) {
logger.error('Google Analytics Event Error >>', err);
}
logger.debug(`${action} timing event successfully sent to google analytics`);
});
},
};

View file

@ -3,11 +3,9 @@ const express = require('express');
const bodyParser = require('body-parser');
const expressHandlebars = require('express-handlebars');
const Handlebars = require('handlebars');
const handlebarsHelpers = require('./helpers/handlebarsHelpers.js');
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
const config = require('./config/speechConfig.js');
const logger = require('winston');
const { getDownloadDirectory } = require('./helpers/lbryApi');
const helmet = require('helmet');
const PORT = 3000; // set port
const app = express(); // create an Express application
@ -54,9 +52,8 @@ app.use(passport.session());
// configure handlebars & register it with express app
const hbs = expressHandlebars.create({
defaultLayout: 'main', // sets the default layout
defaultLayout: 'embed', // sets the default layout
handlebars : Handlebars, // includes basic handlebars for access to that library
helpers : handlebarsHelpers, // custom defined helpers
});
app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars');
@ -67,19 +64,12 @@ app.use(populateLocalsDotUser);
// start the server
db.sequelize
.sync() // sync sequelize
.then(() => { // get the download directory from the daemon
logger.info('Retrieving daemon download directory...');
return getDownloadDirectory();
})
.then(hostedContentPath => {
// add the hosted content folder at a static path
app.use('/media', express.static(hostedContentPath));
// require routes
.then(() => { // require routes
require('./routes/auth-routes.js')(app);
require('./routes/api-routes.js')(app);
require('./routes/page-routes.js')(app);
require('./routes/serve-routes.js')(app);
require('./routes/home-routes.js')(app);
require('./routes/fallback-routes.js')(app);
const http = require('http');
return http.Server(app);
})

View file

@ -176,15 +176,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
this.findOne({
where: {name, claimId},
})
.then(result => {
if (!result) {
return resolve(null);
};
resolve(claimId);
})
.catch(error => {
reject(error);
});
.then(result => {
if (!result) {
return resolve(null);
};
resolve(claimId);
})
.catch(error => {
reject(error);
});
});
};

View file

@ -318,15 +318,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
this.findOne({
where: {name, claimId},
})
.then(result => {
if (!result) {
return resolve(null);
};
resolve(claimId);
})
.catch(error => {
reject(error);
});
.then(result => {
if (!result) {
return resolve(null);
};
resolve(claimId);
})
.catch(error => {
reject(error);
});
});
};

View file

@ -1,7 +1,7 @@
const fs = require('fs');
const path = require('path');
// const fs = require('fs');
// const path = require('path');
const Sequelize = require('sequelize');
const basename = path.basename(module.filename);
// const basename = path.basename(module.filename);
const logger = require('winston');
const config = require('../config/speechConfig.js');
const { database, username, password } = config.sql;
@ -30,16 +30,19 @@ sequelize
logger.error('Sequelize was unable to connect to the database:', err);
});
// add each model to the db object
fs
.readdirSync(__dirname)
.filter(file => {
return (file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js');
})
.forEach(file => {
const model = sequelize['import'](path.join(__dirname, file));
db[model.name] = model;
});
// manually add each model to the db
const Certificate = require('./certificate.js');
const Channel = require('./channel.js');
const Claim = require('./claim.js');
const File = require('./file.js');
const Request = require('./request.js');
const User = require('./user.js');
db['Certificate'] = sequelize.import('Certificate', Certificate);
db['Channel'] = sequelize.import('Channel', Channel);
db['Claim'] = sequelize.import('Claim', Claim);
db['File'] = sequelize.import('File', File);
db['Request'] = sequelize.import('Request', Request);
db['User'] = sequelize.import('User', User);
// run model.association for each model in the db object that has an association
Object.keys(db).forEach(modelName => {

View file

@ -6,12 +6,14 @@
"scripts": {
"test": "mocha --recursive",
"test-all": "mocha --recursive",
"start": "node speech.js",
"start": "node server.js",
"start-dev": "nodemon server.js",
"lint": "eslint .",
"fix": "eslint . --fix",
"precommit": "eslint .",
"babel": "babel",
"webpack": "webpack"
"build-dev": "webpack --config webpack.dev.js",
"build-prod": "webpack --config webpack.prod.js"
},
"repository": {
"type": "git",
@ -35,26 +37,31 @@
"config": "^1.26.1",
"connect-multiparty": "^2.0.0",
"cookie-session": "^2.0.0-beta.3",
"cross-fetch": "^1.1.1",
"express": "^4.15.2",
"express-handlebars": "^3.0.0",
"form-data": "^2.3.1",
"helmet": "^3.8.1",
"mysql2": "^1.3.5",
"nodemon": "^1.11.0",
"node-fetch": "^2.0.0",
"passport": "^0.4.0",
"passport-local": "^1.0.0",
"prop-types": "^15.6.0",
"react": "^16.2.0",
"react-dom": "^16.2.0",
"react-ga": "^2.4.1",
"react-helmet": "^5.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",
"sequelize-cli": "^3.0.0-3",
"sleep": "^5.1.1",
"universal-analytics": "^0.4.13",
"webpack-node-externals": "^1.6.0",
"whatwg-fetch": "^2.0.3",
"winston": "^2.3.1",
"winston-slack-webhook": "billbitt/winston-slack-webhook"
@ -62,21 +69,29 @@
"devDependencies": {
"babel-core": "^6.26.0",
"babel-loader": "^7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1",
"babel-register": "^6.26.0",
"chai": "^4.1.2",
"chai-http": "^3.0.0",
"eslint": "3.19.0",
"eslint-config-standard": "10.2.1",
"eslint-plugin-import": "^2.2.0",
"eslint-plugin-node": "^4.2.2",
"eslint-plugin-promise": "3.5.0",
"eslint-plugin-react": "6.10.3",
"eslint-plugin-standard": "3.0.1",
"css-loader": "^0.28.9",
"eslint": "4.18.0",
"eslint-config-standard": "^10.2.1",
"eslint-config-standard-jsx": "^5.0.0",
"eslint-plugin-import": "^2.8.0",
"eslint-plugin-node": "^4.2.3",
"eslint-plugin-promise": "^3.5.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^3.0.1",
"husky": "^0.13.4",
"mocha": "^4.0.1",
"nodemon": "^1.15.1",
"redux-devtools": "^3.4.1",
"webpack": "^3.10.0"
"regenerator-transform": "^0.12.3",
"webpack": "^3.10.0",
"webpack-merge": "^4.1.2"
}
}

View file

@ -8,19 +8,19 @@ function returnUserAndChannelInfo (userInstance) {
userInfo['id'] = userInstance.id;
userInfo['userName'] = userInstance.userName;
userInstance
.getChannel()
.then(({channelName, channelClaimId}) => {
userInfo['channelName'] = channelName;
userInfo['channelClaimId'] = channelClaimId;
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
resolve(userInfo);
})
.catch(error => {
reject(error);
});
.getChannel()
.then(({channelName, channelClaimId}) => {
userInfo['channelName'] = channelName;
userInfo['channelClaimId'] = channelClaimId;
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
})
.then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId;
resolve(userInfo);
})
.catch(error => {
reject(error);
});
});
}
@ -32,34 +32,34 @@ module.exports = new PassportLocalStrategy(
(username, password, done) => {
logger.debug('logging user in');
return db
.User
.findOne({where: {userName: username}})
.then(user => {
if (!user) {
// logger.debug('no user found');
.User
.findOne({where: {userName: username}})
.then(user => {
if (!user) {
// logger.debug('no user found');
return done(null, false, {message: 'Incorrect username or password'});
}
user.comparePassword(password, (passwordErr, isMatch) => {
if (passwordErr) {
logger.error('passwordErr:', passwordErr);
return done(null, false, {message: passwordErr});
}
if (!isMatch) {
// logger.debug('incorrect password');
return done(null, false, {message: 'Incorrect username or password'});
}
user.comparePassword(password, (passwordErr, isMatch) => {
if (passwordErr) {
logger.error('passwordErr:', passwordErr);
return done(null, false, {message: passwordErr});
}
if (!isMatch) {
// logger.debug('incorrect password');
return done(null, false, {message: 'Incorrect username or password'});
}
logger.debug('Password was a match, returning User');
return returnUserAndChannelInfo(user)
.then((userInfo) => {
return done(null, userInfo);
})
.catch(error => {
return done(error);
});
});
})
.catch(error => {
return done(error);
logger.debug('Password was a match, returning User');
return returnUserAndChannelInfo(user)
.then((userInfo) => {
return done(null, userInfo);
})
.catch(error => {
return done(error);
});
});
})
.catch(error => {
return done(error);
});
}
);

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,26 +526,24 @@ table {
/* Assets */
.asset {
width: 100%;
.asset-holder {
clear : both;
display: inline-block;
width : 31%;
padding: 0px;
margin : 1%;
}
#show-body #asset-boilerpate {
display: none;
.asset-preview {
width : 100%;
padding: 0px;
margin : 0px
}
#showlite-body #asset-display-component {
max-width: 50%;
text-align: center;
}
/* show */
/* video */
#video-asset {
background-color: #000000;
#video {
cursor: pointer;
}
#showlite-body #video-asset {
background-color: #ffffff;
width: calc(100% - 12px - 12px - 2px);
margin: 6px;
@ -533,6 +551,29 @@ table {
border: 1px solid #d0d0d0;
}
/* show lite */
.show-lite-container {
text-align: center;
}
.show-lite-container #asset-display-component {
max-height: calc(100vh - 3em);
}
.show-details-container #asset-display-component .asset {
width: 100%
}
.show-lite-container #asset-display-component .asset {
max-height: calc(100vh - 3em);
max-width: 100vw;
}
#asset-boilerplate {
max-height: 3em;
}
/* item lists */
.content-list-item-asset {

File diff suppressed because it is too large Load diff

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,
name,
shortId,
longId,
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,
name,
value,
data: {
name,
value,
},
};
};
export function updateClaim (value) {
return {
type: actions.CLAIM_UPDATE,
value,
data: value,
};
};
@ -39,30 +41,34 @@ export function setPublishInChannel (channel) {
export function updatePublishStatus (status, message) {
return {
type: actions.PUBLISH_STATUS_UPDATE,
status,
message,
data: {
status,
message,
},
};
};
export function updateError (name, value) {
return {
type: actions.ERROR_UPDATE,
name,
value,
data: {
name,
value,
},
};
};
export function updateSelectedChannel (value) {
export function updateSelectedChannel (channelName) {
return {
type: actions.SELECTED_CHANNEL_UPDATE,
value,
data: channelName,
};
};
export function toggleMetadataInputs (value) {
export function toggleMetadataInputs (showMetadataInputs) {
return {
type: actions.TOGGLE_METADATA_INPUTS,
value,
data: showMetadataInputs,
};
};

View file

@ -1,48 +1,119 @@
import * as actions from 'constants/show_action_types';
// export action creators
export function updateRequestWithChannelRequest (name, id) {
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
// basic request parsing
export function onHandleShowPageUri (params) {
return {
type: actions.REQUEST_UPDATE_CHANNEL,
name,
id,
type: actions.HANDLE_SHOW_URI,
data: params,
};
};
export function updateRequestWithAssetRequest (name, id, channelName, channelId, extension) {
export function onRequestError (error) {
return {
type : actions.REQUEST_UPDATE_CLAIM,
name,
id,
channelName: null,
channelId : null,
extension,
type: actions.REQUEST_ERROR,
data: error,
};
};
export function updateChannelData (name, longId, shortId) {
export function onNewChannelRequest (channelName, channelId) {
const requestType = CHANNEL;
const requestId = `cr#${channelName}#${channelId}`;
return {
type: actions.CHANNEL_DATA_UPDATE,
name,
longId,
shortId,
type: actions.CHANNEL_REQUEST_NEW,
data: { requestType, requestId, channelName, channelId },
};
};
export function updateChannelClaimsData (claims, currentPage, totalPages, totalClaims) {
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.CHANNEL_CLAIMS_DATA_UPDATE,
claims,
currentPage,
totalPages,
totalClaims,
type: actions.ASSET_REQUEST_NEW,
data: {
requestType,
requestId,
name,
modifier: {
id,
channel: {
name: channelName,
id : channelId,
},
},
},
};
};
export function updateAssetClaimData (data, shortId) {
export function onRequestUpdate (requestType, requestId) {
return {
type: actions.ASSET_CLAIM_DATA_UPDATE,
data,
shortId,
type: actions.REQUEST_UPDATE,
data: {
requestType,
requestId,
},
};
};
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,
};
};

38
react/api/assetApi.js Normal file
View file

@ -0,0 +1,38 @@
import Request from 'utils/request';
const { site: { host } } = require('../../config/speechConfig.js');
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: { 'Content-Type': 'application/json' },
body : JSON.stringify(body),
};
// create url
const url = `${host}/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 = `${host}/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 = `${host}/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';
const { site: { host } } = require('../../config/speechConfig.js');
export function getChannelData (name, id) {
console.log('getting channel data for channel:', name, id);
if (!id) id = 'none';
const url = `${host}/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 = `${host}/api/channel/claims/${name}/${longId}/${page}`;
return Request(url);
};

12
react/api/fileApi.js Normal file
View file

@ -0,0 +1,12 @@
import Request from 'utils/request';
const { site: { host } } = require('../../config/speechConfig.js');
export function checkFileAvailability (name, claimId) {
const url = `${host}/api/file/availability/${name}/${claimId}`;
return Request(url);
}
export function triggerClaimGet (name, claimId) {
const url = `${host}/api/claim/get/${name}/${claimId}`;
return Request(url);
}

22
react/app.js Normal file
View file

@ -0,0 +1,22 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import HomePage from 'components/HomePage';
import AboutPage from 'components/AboutPage';
import LoginPage from 'containers/LoginPage';
import ShowPage from 'containers/ShowPage';
import FourOhFourPage from 'components/FourOhFourPage';
const App = () => {
return (
<Switch>
<Route exact path='/' component={HomePage} />
<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>
);
};
export default App;

45
react/client.js Normal file
View file

@ -0,0 +1,45 @@
import React from 'react';
import { hydrate } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, compose } from 'redux';
import { BrowserRouter } from 'react-router-dom';
import Reducer from 'reducers';
import createSagaMiddleware from 'redux-saga';
import rootSaga from 'sagas';
import GAListener from 'components/GAListener';
import App from './app';
// get the state from a global variable injected into the server-generated HTML
const preloadedState = window.__PRELOADED_STATE__ || null;
// Allow the passed state to be garbage-collected
delete window.__PRELOADED_STATE__;
// create and apply middleware
const sagaMiddleware = createSagaMiddleware();
const middleware = applyMiddleware(sagaMiddleware);
const reduxMiddleware = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;
// create teh store
let store;
if (preloadedState) {
store = createStore(Reducer, preloadedState, reduxMiddleware);
} else {
store = createStore(Reducer, reduxMiddleware);
}
// run the saga middlweare
sagaMiddleware.run(rootSaga);
// render the app
hydrate(
<Provider store={store}>
<BrowserRouter>
<GAListener>
<App />
</GAListener>
</BrowserRouter>
</Provider>,
document.getElementById('react-app')
);

View file

@ -1,27 +1,29 @@
import React from 'react';
import NavBar from 'containers/NavBar';
import SEO from 'components/SEO';
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>
<SEO pageTitle={'About'} pageUri={'about'} />
<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>
</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>
<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>

View file

@ -1,133 +1,28 @@
import React from 'react';
import PropTypes from 'prop-types';
import ProgressBar from 'components/ProgressBar';
import Request from 'utils/request';
import { LOCAL_CHECK, SEARCHING, UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states';
import { connect } from 'react-redux';
import View from './view';
import { fileRequested } from 'actions/show';
import { selectAsset } from 'selectors/show';
class AssetDisplay extends React.Component {
constructor (props) {
super(props);
this.state = {
error : null,
status: LOCAL_CHECK,
};
this.isLocalFileAvailableOnServer = this.isLocalFileAvailableOnServer.bind(this);
this.triggerGetAssetOnServer = this.triggerGetAssetOnServer.bind(this);
}
componentDidMount () {
const that = this;
this.isLocalFileAvailableOnServer()
.then(isAvailable => {
if (!isAvailable) {
console.log('file is not yet available');
that.setState({status: SEARCHING});
return that.triggerGetAssetOnServer();
}
})
.then(() => {
that.setState({status: AVAILABLE});
})
.catch(error => {
that.setState({
status: UNAVAILABLE,
error : error.message,
});
});
}
isLocalFileAvailableOnServer () {
console.log(`checking if file is available for ${this.props.name}#${this.props.claimId}`);
const url = `/api/file-is-available/${this.props.name}/${this.props.claimId}`;
return new Promise((resolve, reject) => {
Request(url)
.then(({success, message, data: isAvailable}) => {
if (success) {
console.log('/api/file-is-available response:', isAvailable);
return resolve(isAvailable);
}
reject(new Error(message));
})
.catch(error => {
reject(error);
});
});
}
triggerGetAssetOnServer () {
console.log(`getting claim for ${this.props.name}#${this.props.claimId}`);
const url = `/api/claim-get/${this.props.name}/${this.props.claimId}`;
return new Promise((resolve, reject) => {
Request(url)
.then(({success, message}) => {
console.log('/api/claim-get response:', success, message);
if (success) {
return resolve(true);
}
reject(new Error(message));
})
.catch(error => {
reject(error);
});
});
}
render () {
return (
<div id="asset-display-component">
{(this.state.status === LOCAL_CHECK) &&
<div>
<p>Checking to see if Spee.ch has your asset locally...</p>
</div>
}
{(this.state.status === SEARCHING) &&
<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>
}
{(this.state.status === UNAVAILABLE) &&
<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">{this.state.error}</p></i>
</div>
}
{(this.state.status === AVAILABLE) &&
(() => {
switch (this.props.contentType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
return (
<img className="asset" src={this.props.src} alt={this.props.name}/>
);
case 'image/gif':
return (
<img className="asset" src={this.props.src} alt={this.props.name}/>
);
case 'video/mp4':
return (
<video id="video" className="asset" controls poster={this.props.thumbnail}>
<source src={this.props.src}/>
<p>Your browser does not support the <code>video</code> element.</p>
</video>
);
default:
return (
<p>Unsupported file type</p>
);
}
})()
}
</div>
);
}
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,
};
};
AssetDisplay.propTypes = {
name : PropTypes.string.isRequired,
claimId : PropTypes.string.isRequired,
src : PropTypes.string.isRequired,
contentType: PropTypes.string.isRequired,
fileExt : PropTypes.string.isRequired,
thumbnail : PropTypes.string,
const mapDispatchToProps = dispatch => {
return {
onFileRequest: (name, claimId) => {
dispatch(fileRequested(name, claimId));
},
};
};
export default AssetDisplay;
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -0,0 +1,73 @@
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 () {
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

@ -1,177 +1,14 @@
import React from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from 'selectors/show';
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 () {
return (
<div>
{this.props.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"><a href={`/${this.props.channelName}:${this.props.certificateId}`}>{this.props.channelName}</a></span>
</div>
</div>
}
{this.props.description &&
<div className="row row--padded row--wide row--no-top">
<span className="text">{this.props.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">
<a className="link--primary" href={`/${this.props.shortClaimId}/${this.props.name}.${this.props.fileExt}`}><span
className="text">Link:</span></a>
</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={`${this.props.host}/${this.props.shortClaimId}/${this.props.name}.${this.props.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>
{(this.props.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="${this.props.thumbnail}" src="${this.props.host}/${this.props.claimId}/${this.props.name}.${this.props.fileExt}"/></video>`}/>
) : (
<input type="text" id="embed-text" className="input-disabled input-text--full-width" readOnly
onClick={this.select} spellCheck="false"
value={`<img src="${this.props.host}/${this.props.claimId}/${this.props.name}.${this.props.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=${this.props.host}/${this.props.shortClaimId}/${this.props.name}`}>twitter</a>
<a className="link--primary" target="_blank"
href={`https://www.facebook.com/sharer/sharer.php?u=${this.props.host}/${this.props.shortClaimId}/${this.props.name}`}>facebook</a>
<a className="link--primary" target="_blank"
href={`http://tumblr.com/widgets/share/tool?canonicalUrl=${this.props.host}/${this.props.shortClaimId}/${this.props.name}`}>tumblr</a>
<a className="link--primary" target="_blank"
href={`https://www.reddit.com/submit?url=${this.props.host}/${this.props.shortClaimId}/${this.props.name}&title=${this.props.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">
{this.props.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">
{this.props.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">
{this.props.contentType ? `${this.props.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">
<a className="text link--primary" id="show-details-toggle" href="#" onClick={this.toggleDetails}>{this.state.showDetails ? '[less]' : '[more]'}</a>
</div>
</div>
);
}
const mapStateToProps = ({ show }) => {
// select asset
const asset = selectAsset(show);
// return props
return {
asset,
};
};
AssetInfo.propTypes = {
channelName : PropTypes.string,
certificateId: PropTypes.string,
description : PropTypes.string,
shortId : PropTypes.string.isRequired,
name : PropTypes.string.isRequired,
claimId : PropTypes.string.isRequired,
contentType : PropTypes.string.isRequired,
fileExt : PropTypes.string.isRequired,
thumbnail : PropTypes.string,
host : PropTypes.string.isRequired,
};
export default AssetInfo;
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

@ -3,23 +3,9 @@ import { Link } from 'react-router-dom';
const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
const directSourceLink = `${claimId}/${name}.${fileExt}`;
const showUrlLink = `${claimId}/${name}`;
const previewHolderStyle = {
clear : 'both',
display : 'inline-block',
width : '31%',
padding : '0px',
margin : '1%',
backgroundColor: 'black',
};
const assetStyle = {
width : '100%',
padding: '0px',
margin : '0px',
display: 'block',
};
const showUrlLink = `/${claimId}/${name}`;
return (
<div style={previewHolderStyle}>
<div className='asset-holder'>
<Link to={showUrlLink} >
{(() => {
switch (contentType) {
@ -27,16 +13,16 @@ const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
case 'image/jpg':
case 'image/png':
return (
<img style={assetStyle} className={'asset-preview--image'} src={directSourceLink} alt={name}/>
<img className={'asset-preview'} src={directSourceLink} alt={name} />
);
case 'image/gif':
return (
<img style={assetStyle} className={'asset-preview--gif'} src={directSourceLink} alt={name}/>
<img className={'asset-preview'} src={directSourceLink} alt={name} />
);
case 'video/mp4':
return (
<video style={assetStyle}>
<source src={directSourceLink} type={contentType}/>
<video className={'asset-preview'}>
<source src={directSourceLink} type={contentType} />
</video>
);
default:

View file

@ -1,11 +1,14 @@
import React from 'react';
import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from 'selectors/show';
const AssetTitle = ({title}) => {
return (
<div>
<span className="text--large">{title}</span>
</div>
);
const mapStateToProps = ({ show }) => {
// select title
const { claimData: { title } } = selectAsset(show);
// return props
return {
title,
};
};
export default AssetTitle;
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

@ -1,20 +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>{this.props.error}</p>
<NavBar />
<div className='row row--padded'>
<p>{error}</p>
</div>
</div>
);
}
};
// required props
// error
ErrorPage.propTypes = {
error: PropTypes.string.isRequired,
};
export default ErrorPage;

View file

@ -0,0 +1,24 @@
import React from 'react';
import NavBar from 'containers/NavBar';
import Helmet from 'react-helmet';
const { site: { title, host } } = require('../../../config/speechConfig.js');
class FourOhForPage extends React.Component {
render () {
return (
<div>
<Helmet>
<title>{title} - 404</title>
<link rel='canonical' href={`${host}/404`} />
</Helmet>
<NavBar />
<div className='row row--padded'>
<h2>404</h2>
<p>That page does not exist</p>
</div>
</div>
);
}
};
export default FourOhForPage;

View file

@ -0,0 +1,25 @@
import React from 'react';
import GoogleAnalytics from 'react-ga';
import { withRouter } from 'react-router-dom';
const config = require('../../../config/speechConfig.js');
const googleApiKey = config.analytics.googleId;
GoogleAnalytics.initialize(googleApiKey);
class GAListener extends React.Component {
componentDidMount () {
this.sendPageView(this.props.history.location);
this.props.history.listen(this.sendPageView);
}
sendPageView (location) {
GoogleAnalytics.set({ page: location.pathname });
GoogleAnalytics.pageview(location.pathname);
}
render () {
return this.props.children;
}
}
export default withRouter(GAListener);

View file

@ -1,18 +1,20 @@
import React from 'react';
import SEO from 'components/SEO';
import NavBar from 'containers/NavBar';
import PublishTool from 'containers/PublishTool';
class PublishPage extends React.Component {
class HomePage extends React.Component {
render () {
return (
<div className={'row row--tall flex-container--column'}>
<NavBar/>
<SEO />
<NavBar />
<div className={'row row--tall row--padded flex-container--column'}>
<PublishTool/>
<PublishTool />
</div>
</div>
);
}
};
export default PublishPage;
export default HomePage;

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

@ -5,39 +5,39 @@ import * as publishStates from 'constants/publish_claim_states';
function PublishStatus ({ status, message }) {
return (
<div className="row row--tall flex-container--column flex-container--center-center">
<div className='row row--tall flex-container--column flex-container--center-center'>
{(status === publishStates.LOAD_START) &&
<div className="row align-content-center">
<div className='row align-content-center'>
<p>File is loading to server</p>
<p className="blue">{message}</p>
<p className='blue'>{message}</p>
</div>
}
{(status === publishStates.LOADING) &&
<div>
<div className="row align-content-center">
<div className='row align-content-center'>
<p>File is loading to server</p>
<p className="blue">{message}</p>
<p className='blue'>{message}</p>
</div>
</div>
}
{(status === publishStates.PUBLISHING) &&
<div className="row align-content-center">
<div className='row align-content-center'>
<p>Upload complete. Your file is now being published on the blockchain...</p>
<ProgressBar size={12}/>
<p>Curious what magic is happening here? <a className="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
<ProgressBar size={12} />
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
</div>
}
{(status === publishStates.SUCCESS) &&
<div className="row align-content-center">
<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 className="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) &&
<div className="row align-content-center">
<div className='row align-content-center'>
<p>Something went wrong...</p>
<p><strong>{message}</strong></p>
<p>For help, post the above error text in the #speech channel on the <a className="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">lbry discord</a></p>
<p>For help, post the above error text in the #speech channel on the <a className='link--primary' href='https://discord.gg/YjYbwhS' target='_blank'>lbry discord</a></p>
</div>
}
</div>

View file

@ -0,0 +1,32 @@
import React from 'react';
import Helmet from 'react-helmet';
import PropTypes from 'prop-types';
import { createPageTitle } from 'utils/pageTitle';
import { createMetaTags } from 'utils/metaTags';
import { createCanonicalLink } from 'utils/canonicalLink';
class SEO extends React.Component {
render () {
let { pageTitle, asset, channel, pageUri } = this.props;
pageTitle = createPageTitle(pageTitle);
const metaTags = createMetaTags(asset, channel);
const canonicalLink = createCanonicalLink(asset, channel, pageUri);
return (
<Helmet
title={pageTitle}
meta={metaTags}
link={[{rel: 'canonical', href: canonicalLink}]}
/>
);
}
};
SEO.propTypes = {
pageTitle: PropTypes.string,
pageUri : PropTypes.string,
channel : PropTypes.object,
asset : PropTypes.object,
};
export default SEO;

View file

@ -1,66 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import NavBar from 'containers/NavBar';
import AssetTitle from 'components/AssetTitle';
import AssetDisplay from 'components/AssetDisplay';
import AssetInfo from 'components/AssetInfo';
import { connect } from 'react-redux';
import View from './view';
class ShowAssetDetails extends React.Component {
componentDidMount () {
console.log('ShowAssetDetails props', this.props);
}
render () {
return (
<div>
<NavBar/>
{this.props.error &&
<div className="row row--padded">
<p>{this.props.error}</p>
</div>
}
{this.props.claimData &&
<div className="row row--tall row--padded">
<div className="column column--10">
<AssetTitle title={this.props.claimData.title}/>
</div>
<div className="column column--5 column--sml-10 align-content-top">
<div className="row row--padded">
<AssetDisplay
name={this.props.claimData.name}
claimId={this.props.claimData.claimId}
src={`/${this.props.claimData.claimId}/${this.props.claimData.name}.${this.props.claimData.fileExt}`}
contentType={this.props.claimData.contentType}
fileExt={this.props.claimData.fileExt}
thumbnail={this.props.claimData.thumbnail}
/>
</div>
</div><div className="column column--5 column--sml-10 align-content-top">
<div className="row row--padded">
<AssetInfo
channelName={this.props.claimData.channelName}
certificateId={this.props.claimData.certificateId}
description={this.props.claimData.description}
name={this.props.claimData.name}
claimId={this.props.claimData.claimId}
fileExt={this.props.claimData.fileExt}
contentType={this.props.claimData.contentType}
thumbnail={this.props.claimData.thumbnail}
host={this.props.claimData.host}
shortClaimId={this.props.shortId}
/>
</div>
</div>
</div>
}
</div>
);
}
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,
};
};
ShowAssetDetails.propTypes = {
error : PropTypes.string,
claimData: PropTypes.object.isRequired,
shortId : PropTypes.string.isRequired,
};
export default ShowAssetDetails;
export default connect(mapStateToProps, null)(View);

View file

@ -0,0 +1,42 @@
import React from 'react';
import SEO from 'components/SEO';
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) {
const { name } = asset.claimData;
return (
<div>
<SEO pageTitle={`${name} - details`} asset={asset} />
<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 show-details-container'>
<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

@ -1,36 +1,21 @@
import React from 'react';
import PropTypes from 'prop-types';
import { Link } from 'react-router-dom';
import AssetDisplay from 'components/AssetDisplay';
import { connect } from 'react-redux';
import View from './view';
class ShowLite extends React.Component {
render () {
return (
<div className="row row--tall flex-container--column flex-container--center-center">
{this.props.error &&
<p>{this.props.error}</p>
}
{this.props.claimData &&
<div>
<AssetDisplay
name={this.props.claimData.name}
claimId={this.props.claimData.claimId}
src={`/${this.props.claimData.claimId}/${this.props.claimData.name}.${this.props.claimData.fileExt}`}
contentType={this.props.claimData.contentType}
fileExt={this.props.claimData.fileExt}
thumbnail={this.props.claimData.thumbnail}
/>
<Link id="asset-boilerpate" className="link--primary fine-print" to={`/${this.props.claimData.claimId}/${this.props.claimData.name}`}>hosted via Spee.ch</Link>
</div>
}
</div>
);
}
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,
};
};
ShowLite.propTypes = {
error : PropTypes.string,
claimData: PropTypes.object.isRequired,
};
export default ShowLite;
export default connect(mapStateToProps, null)(View);

View file

@ -0,0 +1,28 @@
import React from 'react';
import SEO from 'components/SEO';
import { Link } from 'react-router-dom';
import AssetDisplay from 'components/AssetDisplay';
class ShowLite extends React.Component {
render () {
const { asset } = this.props;
if (asset) {
const { name, claimId } = asset.claimData;
return (
<div className='row row--tall flex-container--column flex-container--center-center show-lite-container'>
<SEO pageTitle={name} asset={asset} />
<AssetDisplay />
<Link id='asset-boilerpate' className='link--primary fine-print' to={`/${claimId}/${name}`}>hosted
via Spee.ch</Link>
</div>
);
}
return (
<div className='row row--tall row--padded flex-container--column flex-container--center-center'>
<p>loading asset data...</p>
</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,35 @@
import React from 'react';
import SEO from 'components/SEO';
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>
<SEO pageTitle={name} channel={channel} />
<NavBar />
<div className='row row--tall row--padded'>
<div className='column column--10'>
<h2>channel name: {name}</h2>
<p className={'fine-print'}>full channel id: {longId}</p>
<p className={'fine-print'}>short channel id: {shortId}</p>
</div>
<div className='column column--10'>
<ChannelClaimsDisplay />
</div>
</div>
</div>
);
};
return (
<ErrorPage error={'loading channel data...'} />
);
}
};
export default ShowChannel;

View file

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

View file

@ -1,5 +1,21 @@
export const REQUEST_UPDATE_CHANNEL = 'REQUEST_UPDATE_CHANNEL';
export const REQUEST_UPDATE_CLAIM = 'REQUEST_UPDATE_CLAIM';
export const CHANNEL_DATA_UPDATE = 'CHANNEL_DATA_UPDATE';
export const CHANNEL_CLAIMS_DATA_UPDATE = 'CHANNEL_CLAIMS_DATA_UPDATE';
export const ASSET_CLAIM_DATA_UPDATE = 'ASSET_CLAIM_DATA_UPDATE';
// request actions
export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI';
export const REQUEST_ERROR = 'REQUEST_ERROR';
export const REQUEST_UPDATE = 'REQUEST_UPDATE';
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

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

View file

@ -1,27 +1,22 @@
import { connect } from 'react-redux';
import { updateChannelClaimsData } from 'actions/show';
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 {
name : show.showChannel.channelData.name,
longId : show.showChannel.channelData.longId,
claims : show.showChannel.channelClaimsData.claims,
currentPage: show.showChannel.channelClaimsData.currentPage,
totalPages : show.showChannel.channelClaimsData.totalPages,
totalClaims: show.showChannel.channelClaimsData.totalClaims,
channelKey,
channel,
};
};
const mapDispatchToProps = dispatch => {
return {
onChannelClaimsDataUpdate: (claims, currentPage, totalPages, totalClaims) => {
dispatch(updateChannelClaimsData(claims, currentPage, totalPages, totalClaims));
},
onChannelClaimsDataClear: () => {
dispatch(updateChannelClaimsData(null, null, null, null));
},
};
const mapDispatchToProps = {
onUpdateChannelClaims,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,81 +1,50 @@
import React from 'react/index';
import AssetPreview from 'components/AssetPreview/index';
import request from 'utils/request';
import React from 'react';
import AssetPreview from 'components/AssetPreview';
class ChannelClaimsDisplay extends React.Component {
constructor (props) {
super(props);
this.state = {
error: null,
};
this.updateClaimsData = this.updateClaimsData.bind(this);
this.showPreviousResultsPage = this.showPreviousResultsPage.bind(this);
this.showNextResultsPage = this.showNextResultsPage.bind(this);
}
componentDidMount () {
const name = this.props.name;
const longId = this.props.longId;
this.updateClaimsData(name, longId, 1);
}
componentWillReceiveProps (nextProps) {
if (nextProps.name !== this.props.name || nextProps.longId !== this.props.longId) {
this.updateClaimsData(nextProps.name, nextProps.longId, 1);
}
}
updateClaimsData (name, longId, page) {
const url = `/api/channel-claims/${name}/${longId}/${page}`;
const that = this;
return request(url)
.then(({ success, message, data }) => {
console.log('api/channel-claims response:', data);
if (!success) {
return that.setState({error: message});
}
that.setState({error: null}); // move this error to redux state
that.props.onChannelClaimsDataUpdate(data.claims, data.currentPage, data.totalPages, data.totalResults);
})
.catch((error) => {
that.setState({error: error.message});
});
}
componentWillUnmount () {
this.props.onChannelClaimsDataClear();
this.showPreviousResultsPage = this.showPreviousResultsPage.bind(this);
}
showPreviousResultsPage () {
const previousPage = parseInt(this.props.currentPage) - 1;
this.updateClaimsData(this.props.name, this.props.longId, previousPage);
const { channel: { claimsData: { currentPage } } } = this.props;
const previousPage = parseInt(currentPage) - 1;
this.showNewPage(previousPage);
}
showNextResultsPage () {
const nextPage = parseInt(this.props.currentPage) + 1;
this.updateClaimsData(this.props.name, this.props.longId, nextPage);
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>
{this.state.error ? (
<div className="row">
<div className="column column--10">
<p>{this.state.error}</p>
<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>
) : (
<div className="row row--tall">
{this.props.claims &&
<div>
{this.props.claims.map((claim, index) => <AssetPreview
name={claim.name}
claimId={claim.claimId}
fileExt={claim.fileExt}
contentType={claim.contentType}
key={`${claim.name}-${index}`}
/>)}
<div>
{(this.props.currentPage > 1) && <button onClick={this.showPreviousResultsPage}>Previous Page</button>}
{(this.props.currentPage < this.props.totalPages) && <button onClick={this.showNextResultsPage}>Next Page</button>}
</div>
</div>
}
</div>
<p>There are no claims in this channel</p>
)}
</div>
);

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

@ -22,6 +22,6 @@ const mapDispatchToProps = dispatch => {
dispatch(updateSelectedChannel(value));
},
};
}
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux';
import { selectFile, updateError } from 'actions/publish';
import { selectFile, updateError, clearFile } from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
@ -12,13 +12,17 @@ const mapStateToProps = ({ publish }) => {
const mapDispatchToProps = dispatch => {
return {
onFileSelect: (file) => {
selectFile: (file) => {
dispatch(selectFile(file));
dispatch(updateError('publishSubmit', null));
},
onFileError: (value) => {
setFileError: (value) => {
dispatch(clearFile());
dispatch(updateError('file', value));
},
clearFileError: () => {
dispatch(updateError('file', null));
},
};
};

View file

@ -74,20 +74,20 @@ class Dropzone extends React.Component {
try {
validateFile(file); // validate the file's name, type, and size
} catch (error) {
return this.props.onFileError(error.message);
return this.props.setFileError(error.message);
}
// stage it so it will be ready when the publish button is clicked
this.props.onFileError(null);
this.props.onFileSelect(file);
this.props.clearFileError(null);
this.props.selectFile(file);
}
}
render () {
return (
<div className="row row--tall flex-container--column">
<div className='row row--tall flex-container--column'>
<form>
<input className="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onChange={this.handleFileInput} encType="multipart/form-data"/>
<input className='input-file' type='file' id='file_input' name='file_input' accept='video/*,image/*' onChange={this.handleFileInput} encType='multipart/form-data' />
</form>
<div id="preview-dropzone" className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
<div id='preview-dropzone' className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
{this.props.file ? (
<div>
<Preview
@ -95,38 +95,38 @@ class Dropzone extends React.Component {
file={this.props.file}
thumbnail={this.props.thumbnail}
/>
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
{ this.state.dragOver ? (
<div id="dropzone-dragover">
<p className="blue">Drop it.</p>
</div>
) : (
null
)}
{ this.state.mouseOver ? (
<div id="dropzone-instructions">
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
<p>Drag & drop image or video here to publish</p>
<p className="fine-print">OR</p>
<p className="blue--underlined">CHOOSE FILE</p>
</div>
) : (
null
)}
<div id='dropzone-text-holder' className={'flex-container--column flex-container--center-center'}>
{ this.state.dragOver ? (
<div id='dropzone-dragover'>
<p className='blue'>Drop it.</p>
</div>
) : (
null
)}
{ this.state.mouseOver ? (
<div id='dropzone-instructions'>
<p className='info-message-placeholder info-message--failure' id='input-error-file-selection'>{this.props.fileError}</p>
<p>Drag & drop image or video here to publish</p>
<p className='fine-print'>OR</p>
<p className='blue--underlined'>CHOOSE FILE</p>
</div>
) : (
null
)}
</div>
</div>
) : (
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
<div id='dropzone-text-holder' className={'flex-container--column flex-container--center-center'}>
{ this.state.dragOver ? (
<div id="dropzone-dragover">
<p className="blue">Drop it.</p>
<div id='dropzone-dragover'>
<p className='blue'>Drop it.</p>
</div>
) : (
<div id="dropzone-instructions">
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
<div id='dropzone-instructions'>
<p className='info-message-placeholder info-message--failure' id='input-error-file-selection'>{this.props.fileError}</p>
<p>Drag & drop image or video here to publish</p>
<p className="fine-print">OR</p>
<p className="blue--underlined">CHOOSE FILE</p>
<p className='fine-print'>OR</p>
<p className='blue--underlined'>CHOOSE FILE</p>
</div>
)}
</div>

View file

@ -1,10 +1,11 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import SEO from 'components/SEO';
import NavBar from 'containers/NavBar';
import ChannelLoginForm from 'containers/ChannelLoginForm';
import ChannelCreateForm from 'containers/ChannelCreateForm';
class PublishPage extends React.Component {
class LoginPage extends React.Component {
componentWillReceiveProps (newProps) {
// re-route the user to the homepage if the user is logged in
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
@ -15,24 +16,25 @@ class PublishPage 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>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>
<SEO pageTitle={'Login'} pageUri={'login'} />
<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 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);
export default withRouter(LoginPage);

View file

@ -21,29 +21,21 @@ class NavBar extends React.Component {
checkForLoggedInUser () {
const params = {credentials: 'include'};
request('/user', params)
.then(({success, message, data}) => {
if (success) {
this.props.onChannelLogin(data.channelName, data.shortChannelId, data.channelClaimId);
} else {
console.log(message);
}
.then(({ data }) => {
this.props.onChannelLogin(data.channelName, data.shortChannelId, data.channelClaimId);
})
.catch(error => {
console.log('request encountered an error', error);
console.log('/user error:', error.message);
});
}
logoutUser () {
const params = {credentials: 'include'};
request('/logout', params)
.then(({success, message}) => {
if (success) {
this.props.onChannelLogout();
} else {
console.log(message);
}
.then(() => {
this.props.onChannelLogout();
})
.catch(error => {
console.log('request encountered an error', error);
console.log('/logout error', error.message);
});
}
handleSelection (event) {

View file

@ -1,6 +1,5 @@
import {connect} from 'react-redux';
import {clearFile, selectFile, updateError, updatePublishStatus} from 'actions/publish';
import {updateLoggedInChannel} from 'actions/channel';
import {clearFile, updateError, updatePublishStatus} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ channel, publish }) => {
@ -23,15 +22,9 @@ const mapStateToProps = ({ channel, publish }) => {
const mapDispatchToProps = dispatch => {
return {
onFileSelect: (file) => {
dispatch(selectFile(file));
},
onFileClear: () => {
dispatch(clearFile());
},
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
},
onPublishStatusChange: (status, message) => {
dispatch(updatePublishStatus(status, message));
},

View file

@ -11,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 () {
@ -50,35 +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) {
const response = JSON.parse(xhr.response);
console.log('publish response:', response);
if ((xhr.status === 200) && response.success) {
that.props.onPublishStatusChange(publishStates.SUCCESS, response.data.url);
// redirect to the published asset's show page
that.props.history.push(`/${response.data.claimId}/${response.data.name}`);
this.props.history.push(`/${response.data.claimId}/${response.data.name}`);
this.props.onFileClear();
} else {
that.props.onPublishStatusChange(publishStates.FAILED, response.message);
this.props.onPublishStatusChange(publishStates.FAILED, response.message);
}
}
};
@ -106,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]);
}
}
@ -115,60 +110,56 @@ 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);
})
.then(() => {
that.props.onPublishStatusChange('publish request made');
return this.makePublishRequest(this.props.file, metadata);
})
.catch((error) => {
that.props.onPublishSubmitError(error.message);
this.props.onPublishSubmitError(error.message);
});
}
render () {
return (
<div className="row row--no-bottom">
<div className="column column--10">
<div className='row row--no-bottom'>
<div className='column column--10'>
<PublishTitleInput />
</div>
{/* left column */}
<div className="column column--5 column--sml-10" >
<div className="row row--padded">
<div className='column column--5 column--sml-10' >
<div className='row row--padded'>
<Dropzone />
</div>
</div>
{/* right column */}
<div className="column column--5 column--sml-10 align-content-top">
<div id="publish-active-area" className="row row--padded">
<div className="row row--padded row--no-top row--wide">
<div className='column column--5 column--sml-10 align-content-top'>
<div id='publish-active-area' className='row row--padded'>
<div className='row row--padded row--no-top row--wide'>
<PublishUrlInput />
</div>
<div className="row row--padded row--no-top row--wide">
<div className='row row--padded row--no-top row--wide'>
<ChannelSelect />
</div>
{ (this.props.file.type === 'video/mp4') && (
<div className="row row--padded row--no-top row--wide ">
<div className='row row--padded row--no-top row--wide '>
<PublishThumbnailInput />
</div>
)}
<div className="row row--padded row--no-top row--no-bottom row--wide">
<div className='row row--padded row--no-top row--no-bottom row--wide'>
<PublishMetadataInputs />
</div>
<div className="row row--wide align-content-center">
<button id="publish-submit" className="button--primary button--large" onClick={this.publish}>Publish</button>
<div className='row row--wide align-content-center'>
<button id='publish-submit' className='button--primary button--large' onClick={this.publish}>Publish</button>
</div>
<div className="row row--padded row--no-bottom align-content-center">
<button className="button--cancel" onClick={this.props.onFileClear}>Cancel</button>
<div className='row row--padded row--no-bottom align-content-center'>
<button className='button--cancel' onClick={this.props.onFileClear}>Cancel</button>
</div>
<div className="row row--short align-content-center">
<p className="fine-print">By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className="link--primary" target="_blank" href="https://lbry.io/learn">Read more.</a></p>
<div className='row row--short align-content-center'>
<p className='fine-print'>By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className='link--primary' target='_blank' href='https://lbry.io/learn'>Read more.</a></p>
</div>
</div>
</div>

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

@ -17,8 +17,7 @@ const ThumbnailPreview = ({dataUrl}) => {
<img style={imageStyle} src={dataUrl} alt='image preview here' />
) : (
<p>loading...</p>
)
}
)}
</div>
);
}
@ -27,8 +26,12 @@ class PublishThumbnailInput extends React.Component {
constructor (props) {
super(props);
this.state = {
error: null,
};
videoPreviewSrc: null,
thumbnailError : null,
thumbnailInput : '',
}
this.handleInput = this.handleInput.bind(this);
this.updateVideoThumb = this.updateVideoThumb.bind(this);
}
componentDidMount () {
this.setClaimAndThumbailUrl(this.props.publishClaim);

View file

@ -14,6 +14,6 @@ const mapDispatchToProps = dispatch => {
dispatch(updateMetadata(name, value));
},
};
}
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

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

@ -1,26 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
import { updateAssetClaimData } from 'actions/show';
const mapStateToProps = ({ show }) => {
return {
modifier : show.assetRequest.modifier,
claim : show.assetRequest.name,
extension: show.assetRequest.extension,
claimData: show.showAsset.claimData.data,
shortId : show.showAsset.claimData.shortId,
};
};
const mapDispatchToProps = dispatch => {
return {
onAssetClaimDataUpdate: (claimData, shortId) => {
dispatch(updateAssetClaimData(claimData, shortId));
},
onAssetClaimDataClear: () => {
dispatch(updateAssetClaimData(null, null));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,129 +0,0 @@
import React from 'react';
import ShowAssetLite from 'components/ShowAssetLite';
import ShowAssetDetails from 'components/ShowAssetDetails';
import request from 'utils/request';
class ShowAsset extends React.Component {
constructor (props) {
super(props);
this.state = {
error: null,
};
this.getLongClaimId = this.getLongClaimId.bind(this);
this.getClaimData = this.getClaimData.bind(this);
}
componentDidMount () {
console.log('ShowAsset did mount');
console.log('ShowAsset props', this.props);
const modifier = this.props.modifier;
const name = this.props.claim;
// create request params
let body = {};
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),
}
// make request
const that = this;
this.getLongClaimId(params)
.then(claimLongId => {
return Promise.all([that.getShortClaimId(claimLongId, name), that.getClaimData(claimLongId, name)]);
})
.then(([shortId, claimData]) => {
this.setState({error: null}); // note: move this to redux level
this.props.onAssetClaimDataUpdate(claimData, shortId);
})
.catch(error => {
this.setState({error});
});
}
getLongClaimId (params) {
const url = `/api/claim-get-long-id`;
console.log('params:', params);
return new Promise((resolve, reject) => {
request(url, params)
.then(({ success, message, data }) => {
console.log('get long claim id response:', message);
if (!success) {
reject(message);
}
resolve(data);
})
.catch((error) => {
reject(error.message);
});
});
}
getShortClaimId (longId, name) {
const url = `/api/claim-shorten-id/${longId}/${name}`;
return new Promise((resolve, reject) => {
request(url)
.then(({ success, message, data }) => {
console.log('get short claim id response:', data);
if (!success) {
reject(message);
}
resolve(data);
})
.catch((error) => {
reject(error.message);
});
});
}
getClaimData (claimId, claimName) {
return new Promise((resolve, reject) => {
const url = `/api/claim-get-data/${claimName}/${claimId}`;
return request(url)
.then(({ success, message }) => {
console.log('get claim data response:', message);
if (!success) {
reject(message);
}
resolve(message);
})
.catch((error) => {
reject(error.message);
});
});
}
componentWillUnmount () {
this.props.onAssetClaimDataClear();
}
render () {
if (this.props.claimData) {
if (this.props.extension) {
return (
<ShowAssetLite
error={this.state.error}
claimData={this.props.claimData}
/>
);
} else {
return (
<ShowAssetDetails
error={this.state.error}
claimData={this.props.claimData}
shortId={this.props.shortId}
/>
);
}
};
return (
<div></div>
);
}
};
export default ShowAsset;

View file

@ -1,26 +0,0 @@
import { connect } from 'react-redux';
import {updateChannelData} from 'actions/show';
import View from './view';
const mapStateToProps = ({ show }) => {
return {
requestName: show.channelRequest.name,
requestId : show.channelRequest.id,
name : show.showChannel.channelData.name,
shortId : show.showChannel.channelData.shortId,
longId : show.showChannel.channelData.longId,
};
};
const mapDispatchToProps = dispatch => {
return {
onChannelDataUpdate: (name, longId, shortId) => {
dispatch(updateChannelData(name, longId, shortId));
},
onChannelDataClear: () => {
dispatch(updateChannelData(null, null, null));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,69 +0,0 @@
import React from 'react';
import NavBar from 'containers/NavBar';
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
import request from 'utils/request';
class ShowChannel extends React.Component {
constructor (props) {
super(props);
this.state = {
error: null,
};
this.getAndStoreChannelData = this.getAndStoreChannelData.bind(this);
}
componentDidMount () {
this.getAndStoreChannelData(this.props.requestName, this.props.requestId);
}
componentWillReceiveProps (nextProps) {
if (nextProps.requestName !== this.props.requestName || nextProps.requestId !== this.props.requestId) {
this.getAndStoreChannelData(nextProps.requestName, nextProps.requestId);
}
}
getAndStoreChannelData (name, id) {
if (!id) id = 'none';
const url = `/api/channel-data/${name}/${id}`;
const that = this;
return request(url)
.then(({ success, message, data }) => {
console.log('api/channel-data response:', data);
if (!success) {
return that.setState({error: message});
}
that.setState({error: null}); // note: store this error at app level also
that.props.onChannelDataUpdate(data.channelName, data.longChannelClaimId, data.shortChannelClaimId);
})
.catch((error) => {
that.setState({error: error.message});
});
}
componentWillUnmount () {
this.props.onChannelDataClear();
}
render () {
return (
<div>
<NavBar/>
{this.state.error ? (
<div className="row row--tall row--padded">
<div className="column column--10">
<p>{this.state.error}</p>
</div>
</div>
) : (
<div className="row row--tall row--padded">
<div className="column column--10">
<h2>channel name: {this.props.name ? this.props.name : 'loading...'}</h2>
<p>full channel id: {this.props.longId ? this.props.longId : 'loading...'}</p>
<p>short channel id: {this.props.shortId ? this.props.shortId : 'loading...'}</p>
</div>
<div className="column column--10">
{(this.props.name && this.props.longId) && <ChannelClaimsDisplay />}
</div>
</div>
)}
</div>
);
}
};
export default ShowChannel;

View file

@ -1,22 +1,16 @@
import { connect } from 'react-redux';
import { updateRequestWithChannelRequest, updateRequestWithAssetRequest } from 'actions/show';
import { onHandleShowPageUri } from 'actions/show';
import View from './view';
const mapStateToProps = ({ show }) => {
return {
requestType: show.requestType,
error : show.request.error,
requestType: show.request.type,
};
};
const mapDispatchToProps = dispatch => {
return {
onChannelRequest: (name, id) => {
dispatch(updateRequestWithChannelRequest(name, id));
},
onAssetRequest: (name, id, channelName, channelId, extension) => {
dispatch(updateRequestWithAssetRequest(name, id, channelName, channelId, extension));
},
};
const mapDispatchToProps = {
onHandleShowPageUri,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,94 +1,34 @@
import React from 'react';
import ErrorPage from 'components/ErrorPage';
import ShowAsset from 'containers/ShowAsset';
import ShowChannel from 'containers/ShowChannel';
import lbryUri from 'utils/lbryUri';
import ShowAssetLite from 'components/ShowAssetLite';
import ShowAssetDetails from 'components/ShowAssetDetails';
import ShowChannel from 'components/ShowChannel';
import { CHANNEL, ASSET } from 'constants/show_request_types';
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
class ShowPage extends React.Component {
constructor (props) {
super(props);
this.state = {
error: null,
};
this.parseUrlAndUpdateState = this.parseUrlAndUpdateState.bind(this);
this.parseAndUpdateIdentifierAndClaim = this.parseAndUpdateIdentifierAndClaim.bind(this);
this.parseAndUpdateClaimOnly = this.parseAndUpdateClaimOnly.bind(this);
}
componentDidMount () {
console.log('ShowPage did mount');
const identifier = this.props.match.params.identifier;
const claim = this.props.match.params.claim;
this.parseUrlAndUpdateState(identifier, claim);
this.props.onHandleShowPageUri(this.props.match.params);
}
componentWillReceiveProps (nextProps) {
if (nextProps.match.params !== this.props.match.params) {
console.log('ShowPage received new params props');
const identifier = nextProps.match.params.identifier;
const claim = nextProps.match.params.claim;
this.parseUrlAndUpdateState(identifier, claim);
this.props.onHandleShowPageUri(nextProps.match.params);
}
}
parseUrlAndUpdateState (identifier, claim) {
if (identifier) {
return this.parseAndUpdateIdentifierAndClaim(identifier, claim);
}
this.parseAndUpdateClaimOnly(claim);
}
parseAndUpdateIdentifierAndClaim (modifier, claim) {
// 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 this.setState({error: error.message});
}
// update the store
if (isChannel) {
return this.props.onAssetRequest(claimName, null, channelName, channelClaimId, extension);
} else {
return this.props.onAssetRequest(claimName, claimId, null, null, extension);
}
}
parseAndUpdateClaimOnly (claim) {
// 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 this.setState({error: error.message});
}
// return early if this request is for a channel
if (isChannel) {
return this.props.onChannelRequest(channelName, channelClaimId);
}
// if not for a channel, parse the claim request
let claimName, extension; // if I am destructuring below, do I still need to declare these here?
try {
({claimName, extension} = lbryUri.parseClaim(claim));
} catch (error) {
return this.setState({error: error.message});
}
this.props.onAssetRequest(claimName, null, null, null, extension);
}
render () {
console.log('rendering ShowPage');
console.log('ShowPage props', this.props);
if (this.state.error) {
const { error, requestType } = this.props;
if (error) {
return (
<ErrorPage error={this.state.error}/>
<ErrorPage error={error} />
);
}
switch (this.props.requestType) {
switch (requestType) {
case CHANNEL:
return <ShowChannel />;
case ASSET:
return <ShowAsset />;
case ASSET_LITE:
return <ShowAssetLite />;
case ASSET_DETAILS:
return <ShowAssetDetails />;
default:
return <p>loading...</p>;
}

View file

@ -1,15 +0,0 @@
import React from 'react';
import { render } from 'react-dom';
import { createStore } from 'redux';
import Reducer from 'reducers';
import Root from './root';
let store = createStore(
Reducer,
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
);
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

@ -34,27 +34,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, {
@ -62,24 +58,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,
});
case actions.THUMBNAIL_CLAIM_UPDATE:
return Object.assign({}, state, {

View file

@ -1,102 +1,93 @@
import * as actions from 'constants/show_action_types';
import { CHANNEL, ASSET } from 'constants/show_request_types';
import { LOCAL_CHECK, ERROR } from 'constants/asset_display_states';
const initialState = {
requestType : null,
channelRequest: {
name: null,
id : null,
request: {
error: null,
type : null,
id : null,
},
assetRequest: {
name : null,
modifier: {
id : null,
channel: {
name: null,
id : null,
},
},
extension: null,
},
showChannel: {
channelData: {
name : null,
shortId: null,
longId : null,
},
channelClaimsData: {
claims : null,
currentPage: null,
totalPages : null,
totalClaims: null,
},
},
showAsset: {
claimData: {
data : null,
shortId: null,
},
requestList : {},
channelList : {},
assetList : {},
displayAsset: {
error : null,
status: LOCAL_CHECK,
},
};
/*
Reducers describe how the application's state changes in response to actions
*/
export default function (state = initialState, action) {
switch (action.type) {
case actions.REQUEST_UPDATE_CHANNEL:
// handle request
case actions.REQUEST_ERROR:
return Object.assign({}, state, {
requestType : CHANNEL,
channelRequest: {
name: action.name,
id : action.id,
},
request: Object.assign({}, state.request, {
error: action.data,
}),
});
case actions.REQUEST_UPDATE_CLAIM:
case actions.REQUEST_UPDATE:
return Object.assign({}, state, {
requestType : ASSET,
assetRequest: {
name : action.name,
modifier: {
id : action.id,
channel: {
name: action.channelName,
id : action.channelId,
},
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,
},
extension: action.extension,
},
}),
});
case actions.CHANNEL_DATA_UPDATE:
// asset data
case actions.ASSET_ADD:
return Object.assign({}, state, {
showChannel: Object.assign({}, state.showChannel, {
channelData: Object.assign({}, state.channelData, {
name : action.name,
shortId: action.shortId,
longId : action.longId,
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,
}),
}),
});
case actions.CHANNEL_CLAIMS_DATA_UPDATE:
// display an asset
case actions.FILE_AVAILABILITY_UPDATE:
return Object.assign({}, state, {
showChannel: Object.assign({}, state.showChannel, {
channelClaimsData: {
claims : action.claims,
currentPage: action.currentPage,
totalPages : action.totalPages,
totalClaims: action.totalClaims,
},
displayAsset: Object.assign({}, state.displayAsset, {
status: action.data,
}),
});
case actions.ASSET_CLAIM_DATA_UPDATE:
case actions.DISPLAY_ASSET_ERROR:
return Object.assign({}, state, {
showAsset: {
claimData: {
data : action.data,
shortId: action.shortId,
},
},
displayAsset: Object.assign({}, state.displayAsset, {
error : action.data,
status: ERROR,
}),
});
default:
return state;

View file

@ -1,29 +0,0 @@
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';
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} />
</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(),
]);
}

59
react/sagas/show_asset.js Normal file
View file

@ -0,0 +1,59 @@
import { call, put, select, takeLatest } from 'redux-saga/effects';
import * as actions from 'constants/show_action_types';
import { addRequestToRequestList, onRequestError, onRequestUpdate, addAssetToAssetList } from 'actions/show';
import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi';
import { selectShowState } from 'selectors/show';
export function * newAssetRequest (action) {
const { requestType, requestId, name, modifier } = action.data;
// put an action to update the request in redux
yield put(onRequestUpdate(requestType, requestId));
// is this an existing request?
// If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
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,66 @@
import {call, put, select, takeLatest} from 'redux-saga/effects';
import * as actions from 'constants/show_action_types';
import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from 'actions/show';
import { getChannelClaims, getChannelData } from 'api/channelApi';
import { selectShowState } from 'selectors/show';
export function * newChannelRequest (action) {
const { requestType, requestId, channelName, channelId } = action.data;
// put an action to update the request in redux
yield put(onRequestUpdate(requestType, requestId));
// is this an existing request?
// If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
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, newChannelRequest);
};
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);
}

62
react/sagas/show_uri.js Normal file
View file

@ -0,0 +1,62 @@
import { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from 'constants/show_action_types';
import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show';
import { newAssetRequest } from 'sagas/show_asset';
import { newChannelRequest } from 'sagas/show_channel';
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 call(newAssetRequest, onNewAssetRequest(claimName, null, channelName, channelClaimId, extension));
};
yield call(newAssetRequest, 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 call(newChannelRequest, 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 call(newAssetRequest, onNewAssetRequest(claimName, null, null, null, extension));
}
export 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;
};

View file

@ -0,0 +1,37 @@
const { site: { host } } = require('../../config/speechConfig.js');
const createBasicCanonicalLink = (page) => {
if (!page) {
return `${host}`;
};
return `${host}/${page}`;
};
const createAssetCanonicalLink = (asset) => {
let channelName, certificateId, name, claimId;
if (asset.claimData) {
({ channelName, certificateId, name, claimId } = asset.claimData);
};
if (channelName) {
return `${host}/${channelName}:${certificateId}/${name}`;
};
return `${host}/${claimId}/${name}`;
};
const createChannelCanonicalLink = (channel) => {
const { name, longId } = channel;
return `${host}/${name}:${longId}`;
};
export const createCanonicalLink = (asset, channel, page) => {
if (asset) {
return createAssetCanonicalLink(asset);
}
if (channel) {
return createChannelCanonicalLink(channel);
}
if (page) {
return createBasicCanonicalLink(page);
}
return createBasicCanonicalLink();
};

View file

@ -35,4 +35,4 @@ module.exports = {
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
}
},
}
};

View file

@ -4,15 +4,13 @@ module.exports = {
REGEXP_ADDRESS : /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/,
CHANNEL_CHAR : '@',
parseIdentifier : function (identifier) {
console.log('parsing identifier:', 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
const [proto, value, modifierSeperator, modifier] = componentsRegex // eslint-disable-line no-unused-vars
.exec(identifier)
.map(match => match || null);
console.log(`${proto}, ${value}, ${modifierSeperator}, ${modifier}`);
// Validate and process name
if (!value) {
@ -54,15 +52,13 @@ module.exports = {
};
},
parseClaim: function (name) {
console.log('parsing name:', 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
const [proto, claimName, extensionSeperator, extension] = componentsRegex // eslint-disable-line no-unused-vars
.exec(name)
.map(match => match || null);
console.log(`${proto}, ${claimName}, ${extensionSeperator}, ${extension}`);
// Validate and process name
if (!claimName) {

96
react/utils/metaTags.js Normal file
View file

@ -0,0 +1,96 @@
const { site: { title, host, description }, claim: { defaultThumbnail, defaultDescription } } = require('../../config/speechConfig.js');
const determineOgThumbnailContentType = (thumbnail) => {
if (thumbnail) {
const fileExt = thumbnail.substring(thumbnail.lastIndexOf('.'));
switch (fileExt) {
case 'jpeg':
case 'jpg':
return 'image/jpeg';
case 'png':
return 'image/png';
case 'gif':
return 'image/gif';
case 'mp4':
return 'video/mp4';
default:
return 'image/jpeg';
}
}
return '';
};
const createBasicMetaTags = () => {
return [
{property: 'og:title', content: title},
{property: 'og:url', content: host},
{property: 'og:site_name', content: title},
{property: 'og:description', content: description},
{property: 'twitter:site', content: '@spee_ch'},
{property: 'twitter:card', content: 'summary'},
];
};
const createChannelMetaTags = (channel) => {
const { name, longId } = channel;
return [
{property: 'og:title', content: `${name} on ${title}`},
{property: 'og:url', content: `${host}/${name}:${longId}`},
{property: 'og:site_name', content: title},
{property: 'og:description', content: `${name}, a channel on ${title}`},
{property: 'twitter:site', content: '@spee_ch'},
{property: 'twitter:card', content: 'summary'},
];
};
const createAssetMetaTags = (asset) => {
const { claimData } = asset;
const { contentType } = claimData;
const embedUrl = `${host}/${claimData.claimId}/${claimData.name}`;
const showUrl = `${host}/${claimData.claimId}/${claimData.name}`;
const source = `${host}/${claimData.claimId}/${claimData.name}.${claimData.fileExt}`;
const ogTitle = claimData.title || claimData.name;
const ogDescription = claimData.description || defaultDescription;
const ogThumbnailContentType = determineOgThumbnailContentType(claimData.thumbnail);
const ogThumbnail = claimData.thumbnail || defaultThumbnail;
const metaTags = [
{property: 'og:title', content: ogTitle},
{property: 'og:url', content: showUrl},
{property: 'og:site_name', content: title},
{property: 'og:description', content: ogDescription},
{property: 'og:image:width', content: 600},
{property: 'og:image:height', content: 315},
{property: 'twitter:site', content: '@spee_ch'},
];
if (contentType === 'video/mp4' || contentType === 'video/webm') {
metaTags.push({property: 'og:video', content: source});
metaTags.push({property: 'og:video:secure_url', content: source});
metaTags.push({property: 'og:video:type', content: contentType});
metaTags.push({property: 'og:image', content: ogThumbnail});
metaTags.push({property: 'og:image:type', content: ogThumbnailContentType});
metaTags.push({property: 'og:type', content: 'video'});
metaTags.push({property: 'twitter:card', content: 'player'});
metaTags.push({property: 'twitter:player', content: embedUrl});
metaTags.push({property: 'twitter:player:width', content: 600});
metaTags.push({property: 'twitter:text:player_width', content: 600});
metaTags.push({property: 'twitter:player:height', content: 337});
metaTags.push({property: 'twitter:player:stream', content: source});
metaTags.push({property: 'twitter:player:stream:content_type', content: contentType});
} else {
metaTags.push({property: 'og:image', content: source});
metaTags.push({property: 'og:image:type', content: contentType});
metaTags.push({property: 'og:type', content: 'article'});
metaTags.push({property: 'twitter:card', content: 'summary_large_image'});
}
return metaTags;
};
export const createMetaTags = (asset, channel) => {
if (asset) {
return createAssetMetaTags(asset);
};
if (channel) {
return createChannelMetaTags(channel);
};
return createBasicMetaTags();
};

8
react/utils/pageTitle.js Normal file
View file

@ -0,0 +1,8 @@
const { site: { title: siteTitle } } = require('../../config/speechConfig.js');
export const createPageTitle = (pageTitle) => {
if (!pageTitle) {
return `${siteTitle}`;
}
return `${siteTitle} - ${pageTitle}`;
};

View file

@ -1,3 +1,5 @@
import 'cross-fetch/polyfill';
/**
* Parses the JSON returned by a network request
*
@ -13,18 +15,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 +39,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

@ -5,9 +5,9 @@ const multipartMiddleware = multipart({uploadDir: files.uploadDirectory});
const db = require('../models');
const { checkClaimNameAvailability, checkChannelAvailability, publish } = require('../controllers/publishController.js');
const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel, addGetResultsToFileData, createFileData, returnPublishTimingActionType } = require('../helpers/publishHelpers.js');
const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel, addGetResultsToFileData, createFileData } = require('../helpers/publishHelpers.js');
const errorHandlers = require('../helpers/errorHandlers.js');
const { sendGoogleAnalyticsTiming } = require('../helpers/statsHelpers.js');
const { sendGAAnonymousPublishTiming, sendGAChannelPublishTiming } = require('../helpers/googleAnalytics.js');
const { authenticateIfNoUserToken } = require('../auth/authentication.js');
const { getChannelData, getChannelClaims, getClaimId } = require('../controllers/serveController.js');
@ -15,34 +15,73 @@ const NO_CHANNEL = 'NO_CHANNEL';
const NO_CLAIM = 'NO_CLAIM';
module.exports = (app) => {
// route to run a claim_list request on the daemon
app.get('/api/claim-list/:name', ({ ip, originalUrl, params }, res) => {
getClaimList(params.name)
.then(claimsList => {
res.status(200).json(claimsList);
})
.catch(error => {
errorHandlers.handleApiError(originalUrl, ip, error, res);
});
});
// route to see if asset is available locally
app.get('/api/file-is-available/:name/:claimId', ({ ip, originalUrl, params }, res) => {
const name = params.name;
const claimId = params.claimId;
let isAvailable = false;
db.File.findOne({where: {name, claimId}})
// route to check whether site has published to a channel
app.get('/api/channel/availability/:name', ({ ip, originalUrl, params }, res) => {
checkChannelAvailability(params.name)
.then(result => {
if (result) {
isAvailable = true;
if (result === true) {
res.status(200).json(true);
} else {
res.status(200).json(false);
}
res.status(200).json({success: true, data: isAvailable});
})
.catch(error => {
errorHandlers.handleApiError(originalUrl, ip, error, res);
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
// route to get a short channel id from long channel Id
app.get('/api/channel/short-id/:longId/:name', ({ ip, originalUrl, params }, res) => {
db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name)
.then(shortId => {
res.status(200).json(shortId);
})
.catch(error => {
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
app.get('/api/channel/data/:channelName/:channelClaimId', ({ ip, originalUrl, body, params }, res) => {
const channelName = params.channelName;
let channelClaimId = params.channelClaimId;
if (channelClaimId === 'none') channelClaimId = null;
getChannelData(channelName, channelClaimId, 0)
.then(data => {
if (data === NO_CHANNEL) {
return res.status(404).json({success: false, message: 'No matching channel was found'});
}
res.status(200).json({success: true, data});
})
.catch(error => {
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
app.get('/api/channel/claims/:channelName/:channelClaimId/:page', ({ ip, originalUrl, body, params }, res) => {
const channelName = params.channelName;
let channelClaimId = params.channelClaimId;
if (channelClaimId === 'none') channelClaimId = null;
const page = params.page;
getChannelClaims(channelName, channelClaimId, page)
.then(data => {
if (data === NO_CHANNEL) {
return res.status(404).json({success: false, message: 'No matching channel was found'});
}
res.status(200).json({success: true, data});
})
.catch(error => {
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
// route to run a claim_list request on the daemon
app.get('/api/claim/list/:name', ({ ip, originalUrl, params }, res) => {
getClaimList(params.name)
.then(claimsList => {
res.status(200).json(claimsList);
})
.catch(error => {
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
// route to get an asset
app.get('/api/claim-get/:name/:claimId', ({ ip, originalUrl, params }, res) => {
app.get('/api/claim/get/:name/:claimId', ({ ip, originalUrl, params }, res) => {
const name = params.name;
const claimId = params.claimId;
// resolve the claim
@ -64,27 +103,12 @@ module.exports = (app) => {
res.status(200).json({ success: true, message, completed });
})
.catch(error => {
errorHandlers.handleApiError(originalUrl, ip, error, res);
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
// route to check whether this site published to a claim
app.get('/api/claim-is-available/:name', ({ params }, res) => {
app.get('/api/claim/availability/:name', ({ ip, originalUrl, params }, res) => {
checkClaimNameAvailability(params.name)
.then(result => {
if (result === true) {
res.status(200).json(true);
} else {
res.status(200).json(false);
}
})
.catch(error => {
res.status(500).json(error);
});
});
// route to check whether site has published to a channel
app.get('/api/channel-is-available/:name', ({ params }, res) => {
checkChannelAvailability(params.name)
.then(result => {
if (result === true) {
res.status(200).json(true);
@ -93,29 +117,27 @@ module.exports = (app) => {
}
})
.catch(error => {
res.status(500).json(error);
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
// route to run a resolve request on the daemon
app.get('/api/claim-resolve/:name/:claimId', ({ headers, ip, originalUrl, params }, res) => {
app.get('/api/claim/resolve/:name/:claimId', ({ headers, ip, originalUrl, params }, res) => {
resolveUri(`${params.name}#${params.claimId}`)
.then(resolvedUri => {
res.status(200).json(resolvedUri);
})
.catch(error => {
errorHandlers.handleApiError(originalUrl, ip, error, res);
});
.then(resolvedUri => {
res.status(200).json(resolvedUri);
})
.catch(error => {
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
// route to run a publish request on the daemon
app.post('/api/claim-publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => {
app.post('/api/claim/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => {
logger.debug('api/claim-publish body:', body);
logger.debug('api/claim-publish files:', files);
// record the start time of the request and create variable for storing the action type
const publishStartTime = Date.now();
logger.debug('publish request started @', publishStartTime);
let timingActionType;
// define variables
let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, channelName, channelPassword;
// record the start time of the request
const publishStartTime = Date.now();
// validate the body and files of the request
try {
// validateApiPublishRequest(body, files);
@ -123,108 +145,62 @@ module.exports = (app) => {
({fileName, filePath, fileType} = parsePublishApiRequestFiles(files));
({channelName, channelPassword} = parsePublishApiChannel(body, user));
} catch (error) {
logger.debug('publish request rejected, insufficient request parameters', error);
return res.status(400).json({success: false, message: error.message});
}
// check channel authorization
authenticateIfNoUserToken(channelName, channelPassword, user)
.then(authenticated => {
if (!authenticated) {
throw new Error('Authentication failed, you do not have access to that channel');
}
// make sure the claim name is available
return checkClaimNameAvailability(name);
})
.then(result => {
if (!result) {
throw new Error('That name is already claimed by another user.');
}
// create publish parameters object
return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName);
})
.then(publishParams => {
// set the timing event type for reporting
timingActionType = returnPublishTimingActionType(publishParams.channel_name);
// publish the asset
return publish(publishParams, fileName, fileType);
})
.then(result => {
res.status(200).json({
success: true,
message: 'publish completed successfully',
data : {
name,
claimId: result.claim_id,
url : `${site.host}/${result.claim_id}/${name}`,
lbryTx : result,
},
.then(authenticated => {
if (!authenticated) {
throw new Error('Authentication failed, you do not have access to that channel');
}
// make sure the claim name is available
return checkClaimNameAvailability(name);
})
.then(result => {
if (!result) {
throw new Error('That name is already claimed by another user.');
}
// create publish parameters object
return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName);
})
.then(publishParams => {
// publish the asset
return publish(publishParams, fileName, fileType);
})
.then(result => {
res.status(200).json({
success: true,
message: 'publish completed successfully',
data : {
name,
claimId: result.claim_id,
url : `${site.host}/${result.claim_id}/${name}`,
lbryTx : result,
},
});
// record the publish end time and send to google analytics
const publishEndTime = Date.now();
if (channelName) {
sendGAChannelPublishTiming(headers, ip, originalUrl, publishStartTime, publishEndTime);
} else {
sendGAAnonymousPublishTiming(headers, ip, originalUrl, publishStartTime, publishEndTime);
}
})
.catch(error => {
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
// log the publish end time
const publishEndTime = Date.now();
logger.debug('publish request completed @', publishEndTime);
sendGoogleAnalyticsTiming(timingActionType, headers, ip, originalUrl, publishStartTime, publishEndTime);
})
.catch(error => {
errorHandlers.handleApiError(originalUrl, ip, error, res);
});
});
// route to get a short claim id from long claim Id
app.get('/api/claim-shorten-id/:longId/:name', ({ params }, res) => {
app.get('/api/claim/short-id/:longId/:name', ({ ip, originalUrl, body, params }, res) => {
db.Claim.getShortClaimIdFromLongClaimId(params.longId, params.name)
.then(shortId => {
res.status(200).json({success: true, data: shortId});
})
.catch(error => {
logger.error('api error getting short channel id', error);
res.status(200).json({success: false, message: 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);
})
.catch(error => {
logger.error('api error getting short channel id', error);
errorHandlers.handleApiError(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) // getChannelViewData(channelName, channelId, 0)
.then(data => {
if (data === NO_CHANNEL) {
return res.status(200).json({success: false, message: 'No matching channel was found'});
}
res.status(200).json({success: true, data});
})
.catch(error => {
logger.error('api error getting channel contents', error);
errorHandlers.handleApiError(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)// getChannelViewData(channelName, channelClaimId, page)
.then(data => {
if (data === NO_CHANNEL) {
return res.status(200).json({success: false, message: 'No matching channel was found'});
}
res.status(200).json({success: true, data});
})
.catch(error => {
logger.error('api error getting channel contents', error);
errorHandlers.handleApiError(originalUrl, ip, error, res);
});
});
app.post('/api/claim-get-long-id', ({ ip, originalUrl, body, params }, res) => {
app.post('/api/claim/long-id', ({ ip, originalUrl, body, params }, res) => {
logger.debug('body:', body);
const channelName = body.channelName;
const channelClaimId = body.channelClaimId;
@ -233,32 +209,45 @@ module.exports = (app) => {
getClaimId(channelName, channelClaimId, claimName, claimId)
.then(result => {
if (result === NO_CHANNEL) {
return res.status(200).json({success: false, message: 'No matching channel could be found'});
return res.status(404).json({success: false, message: 'No matching channel could be found'});
}
if (result === NO_CLAIM) {
return res.status(200).json({success: false, message: 'No matching claim id could be found'});
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 long claim id', error);
errorHandlers.handleApiError(originalUrl, ip, error, res);
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
});
});
app.get('/api/claim-get-data/:claimName/:claimId', ({ ip, originalUrl, body, params }, 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(200).json({success: false, message: 'No claim could be found'});
return res.status(404).json({success: false, message: 'No claim could be found'});
}
res.status(200).json({success: true, message: claimInfo});
res.status(200).json({success: true, data: claimInfo});
})
.catch(error => {
logger.error('api error getting long claim id', error);
errorHandlers.handleApiError(originalUrl, ip, error, res);
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,
});
@ -49,7 +49,7 @@ module.exports = (app) => {
if (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

@ -0,0 +1,9 @@
const handlePageRender = require('../helpers/handlePageRender.jsx');
module.exports = app => {
// a catch-all route if someone visits a page that does not exist
app.use('*', (req, res) => {
// send response
handlePageRender(req, res);
});
};

View file

@ -1,11 +0,0 @@
module.exports = app => {
// route for the home page
app.get('/', (req, res) => {
res.status(200).render('index');
});
// a catch-all route if someone visits a page that does not exist
app.use('*', ({ originalUrl, ip }, res) => {
// send response
res.status(404).render('404');
});
};

View file

@ -1,24 +1,29 @@
const { site } = require('../config/speechConfig.js');
const handlePageRender = require('../helpers/handlePageRender.jsx');
module.exports = (app) => {
// route for the home page
app.get('/', (req, res) => {
handlePageRender(req, res);
});
// route to display login page
app.get('/login', (req, res) => {
res.status(200).render('index');
handlePageRender(req, res);
});
// route to show 'about' page
app.get('/about', (req, res) => {
res.status(200).render('index');
handlePageRender(req, res);
});
// 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) => {
res.status(200).render('index');
app.get('/popular', (req, res) => {
handlePageRender(req, res);
});
// route to display a list of the trending images
app.get('/new', ({ ip, originalUrl }, res) => {
res.status(200).render('index');
app.get('/new', (req, res) => {
handlePageRender(req, res);
});
// route to send embedable video player (for twitter)
app.get('/embed/:claimId/:name', ({ params }, res) => {

Some files were not shown because too many files have changed in this diff Show more