350 open graph react #360

Merged
bones7242 merged 30 commits from 350-open-graph-react into master 2018-02-24 17:55:00 +01:00
65 changed files with 873 additions and 25733 deletions

View file

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

3
.gitignore vendored
View file

@ -2,3 +2,6 @@ node_modules
.idea .idea
config/sequelizeCliConfig.js config/sequelizeCliConfig.js
config/speechConfig.js config/speechConfig.js
public/bundle
server.js
webpack.config.js

View file

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

View file

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

View file

@ -23,8 +23,10 @@ module.exports = {
uploadDirectory: null, // enter file path to where uploads/publishes should be stored uploadDirectory: null, // enter file path to where uploads/publishes should be stored
}, },
site: { site: {
name: 'Spee.ch', title: 'Spee.ch',
host: 'https://spee.ch', name : 'Spee.ch',
host : 'https://spee.ch',
description: 'Open-source, decentralized image and video sharing.'
}, },
claim: { claim: {
defaultTitle : 'Spee.ch', defaultTitle : 'Spee.ch',

View file

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

View file

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

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,113 +0,0 @@
const Handlebars = require('handlebars');
const { site, 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);
},
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}`);
}
},
};

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

@ -3,11 +3,9 @@ const express = require('express');
const bodyParser = require('body-parser'); const bodyParser = require('body-parser');
const expressHandlebars = require('express-handlebars'); const expressHandlebars = require('express-handlebars');
const Handlebars = require('handlebars'); const Handlebars = require('handlebars');
const handlebarsHelpers = require('./helpers/handlebarsHelpers.js');
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js'); const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
const config = require('./config/speechConfig.js'); const config = require('./config/speechConfig.js');
const logger = require('winston'); const logger = require('winston');
const { getDownloadDirectory } = require('./helpers/lbryApi');
const helmet = require('helmet'); const helmet = require('helmet');
const PORT = 3000; // set port const PORT = 3000; // set port
const app = express(); // create an Express application const app = express(); // create an Express application
@ -54,9 +52,8 @@ app.use(passport.session());
// configure handlebars & register it with express app // configure handlebars & register it with express app
const hbs = expressHandlebars.create({ 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 handlebars : Handlebars, // includes basic handlebars for access to that library
helpers : handlebarsHelpers, // custom defined helpers
}); });
app.engine('handlebars', hbs.engine); app.engine('handlebars', hbs.engine);
app.set('view engine', 'handlebars'); app.set('view engine', 'handlebars');
@ -67,19 +64,12 @@ app.use(populateLocalsDotUser);
// start the server // start the server
db.sequelize db.sequelize
.sync() // sync sequelize .sync() // sync sequelize
.then(() => { // get the download directory from the daemon .then(() => { // require routes
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
require('./routes/auth-routes.js')(app); require('./routes/auth-routes.js')(app);
require('./routes/api-routes.js')(app); require('./routes/api-routes.js')(app);
require('./routes/page-routes.js')(app); require('./routes/page-routes.js')(app);
require('./routes/serve-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'); const http = require('http');
return http.Server(app); return http.Server(app);
}) })

View file

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

View file

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

View file

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

View file

@ -6,12 +6,14 @@
"scripts": { "scripts": {
"test": "mocha --recursive", "test": "mocha --recursive",
"test-all": "mocha --recursive", "test-all": "mocha --recursive",
"start": "node speech.js", "start": "node server.js",
"start-dev": "nodemon server.js",
"lint": "eslint .", "lint": "eslint .",
"fix": "eslint . --fix", "fix": "eslint . --fix",
"precommit": "eslint .", "precommit": "eslint .",
"babel": "babel", "babel": "babel",
"webpack": "webpack" "build-dev": "webpack --config webpack.dev.js",
"build-prod": "webpack --config webpack.prod.js"
}, },
"repository": { "repository": {
"type": "git", "type": "git",
@ -35,18 +37,20 @@
"config": "^1.26.1", "config": "^1.26.1",
"connect-multiparty": "^2.0.0", "connect-multiparty": "^2.0.0",
"cookie-session": "^2.0.0-beta.3", "cookie-session": "^2.0.0-beta.3",
"cross-fetch": "^1.1.1",
"express": "^4.15.2", "express": "^4.15.2",
"express-handlebars": "^3.0.0", "express-handlebars": "^3.0.0",
"form-data": "^2.3.1", "form-data": "^2.3.1",
"helmet": "^3.8.1", "helmet": "^3.8.1",
"mysql2": "^1.3.5", "mysql2": "^1.3.5",
"nodemon": "^1.11.0", "node-fetch": "^2.0.0",
"passport": "^0.4.0", "passport": "^0.4.0",
"passport-local": "^1.0.0", "passport-local": "^1.0.0",
"prop-types": "^15.6.0", "prop-types": "^15.6.0",
"react": "^16.2.0", "react": "^16.2.0",
"react-dom": "^16.2.0", "react-dom": "^16.2.0",
"react-ga": "^2.4.1", "react-ga": "^2.4.1",
"react-helmet": "^5.2.0",
"react-redux": "^5.0.6", "react-redux": "^5.0.6",
"react-router-dom": "^4.2.2", "react-router-dom": "^4.2.2",
"redux": "^3.7.2", "redux": "^3.7.2",
@ -57,6 +61,7 @@
"sequelize-cli": "^3.0.0-3", "sequelize-cli": "^3.0.0-3",
"sleep": "^5.1.1", "sleep": "^5.1.1",
"universal-analytics": "^0.4.13", "universal-analytics": "^0.4.13",
"webpack-node-externals": "^1.6.0",
"whatwg-fetch": "^2.0.3", "whatwg-fetch": "^2.0.3",
"winston": "^2.3.1", "winston": "^2.3.1",
"winston-slack-webhook": "billbitt/winston-slack-webhook" "winston-slack-webhook": "billbitt/winston-slack-webhook"
@ -64,22 +69,29 @@
"devDependencies": { "devDependencies": {
"babel-core": "^6.26.0", "babel-core": "^6.26.0",
"babel-loader": "^7.1.2", "babel-loader": "^7.1.2",
"babel-plugin-transform-object-rest-spread": "^6.26.0",
"babel-polyfill": "^6.26.0", "babel-polyfill": "^6.26.0",
"babel-preset-es2015": "^6.24.1", "babel-preset-es2015": "^6.24.1",
"babel-preset-react": "^6.24.1", "babel-preset-react": "^6.24.1",
"babel-preset-stage-2": "^6.24.1", "babel-preset-stage-2": "^6.24.1",
"babel-register": "^6.26.0",
"chai": "^4.1.2", "chai": "^4.1.2",
"chai-http": "^3.0.0", "chai-http": "^3.0.0",
"eslint": "3.19.0", "css-loader": "^0.28.9",
"eslint-config-standard": "10.2.1", "eslint": "4.18.0",
"eslint-plugin-import": "^2.2.0", "eslint-config-standard": "^10.2.1",
"eslint-plugin-node": "^4.2.2", "eslint-config-standard-jsx": "^5.0.0",
"eslint-plugin-promise": "3.5.0", "eslint-plugin-import": "^2.8.0",
"eslint-plugin-react": "6.10.3", "eslint-plugin-node": "^4.2.3",
"eslint-plugin-standard": "3.0.1", "eslint-plugin-promise": "^3.5.0",
"eslint-plugin-react": "^7.6.1",
"eslint-plugin-standard": "^3.0.1",
"husky": "^0.13.4", "husky": "^0.13.4",
"mocha": "^4.0.1", "mocha": "^4.0.1",
"nodemon": "^1.15.1",
"redux-devtools": "^3.4.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['id'] = userInstance.id;
userInfo['userName'] = userInstance.userName; userInfo['userName'] = userInstance.userName;
userInstance userInstance
.getChannel() .getChannel()
.then(({channelName, channelClaimId}) => { .then(({channelName, channelClaimId}) => {
userInfo['channelName'] = channelName; userInfo['channelName'] = channelName;
userInfo['channelClaimId'] = channelClaimId; userInfo['channelClaimId'] = channelClaimId;
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName); return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
}) })
.then(shortChannelId => { .then(shortChannelId => {
userInfo['shortChannelId'] = shortChannelId; userInfo['shortChannelId'] = shortChannelId;
resolve(userInfo); resolve(userInfo);
}) })
.catch(error => { .catch(error => {
reject(error); reject(error);
}); });
}); });
} }
@ -32,34 +32,34 @@ module.exports = new PassportLocalStrategy(
(username, password, done) => { (username, password, done) => {
logger.debug('logging user in'); logger.debug('logging user in');
return db return db
.User .User
.findOne({where: {userName: username}}) .findOne({where: {userName: username}})
.then(user => { .then(user => {
if (!user) { if (!user) {
// logger.debug('no user found'); // 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'}); return done(null, false, {message: 'Incorrect username or password'});
} }
user.comparePassword(password, (passwordErr, isMatch) => { logger.debug('Password was a match, returning User');
if (passwordErr) { return returnUserAndChannelInfo(user)
logger.error('passwordErr:', passwordErr); .then((userInfo) => {
return done(null, false, {message: passwordErr}); return done(null, userInfo);
} })
if (!isMatch) { .catch(error => {
// logger.debug('incorrect password'); return done(error);
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);
}); });
})
.catch(error => {
return done(error);
});
} }
); );

File diff suppressed because it is too large Load diff

View file

@ -3,7 +3,7 @@ import * as actions from 'constants/show_action_types';
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types'; import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
// basic request parsing // basic request parsing
export function handleShowPageUri (params) { export function onHandleShowPageUri (params) {
return { return {
type: actions.HANDLE_SHOW_URI, type: actions.HANDLE_SHOW_URI,
data: params, data: params,
@ -12,7 +12,7 @@ export function handleShowPageUri (params) {
export function onRequestError (error) { export function onRequestError (error) {
return { return {
type: actions.REQUEST_UPDATE_ERROR, type: actions.REQUEST_ERROR,
data: error, data: error,
}; };
}; };
@ -46,6 +46,16 @@ export function onNewAssetRequest (name, id, channelName, channelId, extension)
}; };
}; };
export function onRequestUpdate (requestType, requestId) {
return {
type: actions.REQUEST_UPDATE,
data: {
requestType,
requestId,
},
};
};
export function addRequestToRequestList (id, error, key) { export function addRequestToRequestList (id, error, key) {
return { return {
type: actions.REQUEST_LIST_ADD, type: actions.REQUEST_LIST_ADD,

View file

@ -1,4 +1,5 @@
import Request from 'utils/request'; import Request from 'utils/request';
const { site: { host } } = require('../../config/speechConfig.js');
export function getLongClaimId (name, modifier) { export function getLongClaimId (name, modifier) {
// console.log('getting long claim id for asset:', name, modifier); // console.log('getting long claim id for asset:', name, modifier);
@ -15,25 +16,23 @@ export function getLongClaimId (name, modifier) {
body['claimName'] = name; body['claimName'] = name;
const params = { const params = {
method : 'POST', method : 'POST',
headers: new Headers({ headers: { 'Content-Type': 'application/json' },
'Content-Type': 'application/json', body : JSON.stringify(body),
}), };
body: JSON.stringify(body),
}
// create url // create url
const url = `/api/claim/long-id`; const url = `${host}/api/claim/long-id`;
// return the request promise // return the request promise
return Request(url, params); return Request(url, params);
}; };
export function getShortId (name, claimId) { export function getShortId (name, claimId) {
// console.log('getting short id for asset:', name, claimId); // console.log('getting short id for asset:', name, claimId);
const url = `/api/claim/short-id/${claimId}/${name}`; const url = `${host}/api/claim/short-id/${claimId}/${name}`;
return Request(url); return Request(url);
}; };
export function getClaimData (name, claimId) { export function getClaimData (name, claimId) {
// console.log('getting claim data for asset:', name, claimId); // console.log('getting claim data for asset:', name, claimId);
const url = `/api/claim/data/${name}/${claimId}`; const url = `${host}/api/claim/data/${name}/${claimId}`;
return Request(url); return Request(url);
}; };

View file

@ -1,16 +1,16 @@
import Request from 'utils/request'; import Request from 'utils/request';
import request from '../utils/request'; const { site: { host } } = require('../../config/speechConfig.js');
export function getChannelData (name, id) { export function getChannelData (name, id) {
console.log('getting channel data for channel:', name, id); console.log('getting channel data for channel:', name, id);
if (!id) id = 'none'; if (!id) id = 'none';
const url = `/api/channel/data/${name}/${id}`; const url = `${host}/api/channel/data/${name}/${id}`;
return request(url); return Request(url);
}; };
export function getChannelClaims (name, longId, page) { export function getChannelClaims (name, longId, page) {
console.log('getting channel claims for channel:', name, longId); console.log('getting channel claims for channel:', name, longId);
if (!page) page = 1; if (!page) page = 1;
const url = `/api/channel/claims/${name}/${longId}/${page}`; const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
return Request(url); return Request(url);
}; };

View file

@ -1,11 +1,12 @@
import Request from 'utils/request'; import Request from 'utils/request';
const { site: { host } } = require('../../config/speechConfig.js');
export function checkFileAvailability (name, claimId) { export function checkFileAvailability (name, claimId) {
const url = `/api/file/availability/${name}/${claimId}`; const url = `${host}/api/file/availability/${name}/${claimId}`;
return Request(url); return Request(url);
} }
export function triggerClaimGet (name, claimId) { export function triggerClaimGet (name, claimId) {
const url = `/api/claim/get/${name}/${claimId}`; const url = `${host}/api/claim/get/${name}/${claimId}`;
return Request(url); 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 @@
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import React from 'react';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import { hydrate } from 'react-dom';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import { Provider } from 'react-redux';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import { createStore, applyMiddleware, compose } from 'redux';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import { BrowserRouter } from 'react-router-dom';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import Reducer from 'reducers';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import createSagaMiddleware from 'redux-saga';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import rootSaga from 'sagas';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import GAListener from 'components/GAListener';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
import App from './app';
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
// get the state from a global variable injected into the server-generated HTML
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
const preloadedState = window.__PRELOADED_STATE__ || null;
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
// Allow the passed state to be garbage-collected
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
delete window.__PRELOADED_STATE__;
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
// create and apply middleware
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
const sagaMiddleware = createSagaMiddleware();
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
const middleware = applyMiddleware(sagaMiddleware);
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
const reduxMiddleware = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
// create teh store
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
let store;
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
if (preloadedState) {
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
store = createStore(Reducer, preloadedState, reduxMiddleware);
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
} else {
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
store = createStore(Reducer, reduxMiddleware);
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
}
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
// run the saga middlweare
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
sagaMiddleware.run(rootSaga);
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
// render the app
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
hydrate(
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
<Provider store={store}>
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
<BrowserRouter>
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
<GAListener>
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
<App />
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
</GAListener>
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
</BrowserRouter>
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
</Provider>,
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
document.getElementById('react-app')
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.
);
neb-b commented 2018-02-23 20:57:29 +01:00 (Migrated from github.com)
Review

Probably don't need these console.logs here.

Probably don't need these `console.log`s here.

View file

@ -1,27 +1,29 @@
import React from 'react'; import React from 'react';
import NavBar from 'containers/NavBar'; import NavBar from 'containers/NavBar';
import SEO from 'components/SEO';
class AboutPage extends React.Component { class AboutPage extends React.Component {
render () { render () {
return ( return (
<div> <div>
<NavBar/> <SEO pageTitle={'About'} pageUri={'about'} />
<div className="row row--padded"> <NavBar />
<div className="column column--5 column--med-10 align-content-top"> <div className='row row--padded'>
<div className="column column--8 column--med-10"> <div className='column column--5 column--med-10 align-content-top'>
<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> <div className='column column--8 column--med-10'>
<p><a className="link--primary" target="_blank" href="https://twitter.com/spee_ch">TWITTER</a></p> <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://github.com/lbryio/spee.ch">GITHUB</a></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://discord.gg/YjYbwhS">DISCORD CHANNEL</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://github.com/lbryio/spee.ch/blob/master/README.md">DOCUMENTATION</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><div className="column column--5 column--med-10 align-content-top"> </div><div className='column column--5 column--med-10 align-content-top'>
<div className="column column--8 column--med-10"> <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 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> <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> <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 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 want to improve spee.ch, join our <a className='link--primary' href='https://discord.gg/YjYbwhS'>discord channel</a> or solve one of our <a className='link--primary' href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p>
</div> </div>
</div> </div>
</div> </div>

View file

@ -8,7 +8,6 @@ class AssetDisplay extends React.Component {
this.props.onFileRequest(name, claimId); this.props.onFileRequest(name, claimId);
} }
render () { render () {
console.log('rendering assetdisplay', this.props);
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props; const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
return ( return (
<div id="asset-display-component"> <div id="asset-display-component">

View file

@ -3,9 +3,9 @@ import { Link } from 'react-router-dom';
const AssetPreview = ({ name, claimId, fileExt, contentType }) => { const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
const directSourceLink = `${claimId}/${name}.${fileExt}`; const directSourceLink = `${claimId}/${name}.${fileExt}`;
const showUrlLink = `${claimId}/${name}`; const showUrlLink = `/${claimId}/${name}`;
return ( return (
<div className="asset-holder"> <div className='asset-holder'>
<Link to={showUrlLink} > <Link to={showUrlLink} >
{(() => { {(() => {
switch (contentType) { switch (contentType) {
@ -13,16 +13,16 @@ const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
case 'image/jpg': case 'image/jpg':
case 'image/png': case 'image/png':
return ( return (
<img className={'asset-preview'} src={directSourceLink} alt={name}/> <img className={'asset-preview'} src={directSourceLink} alt={name} />
); );
case 'image/gif': case 'image/gif':
return ( return (
<img className={'asset-preview'} src={directSourceLink} alt={name}/> <img className={'asset-preview'} src={directSourceLink} alt={name} />
); );
case 'video/mp4': case 'video/mp4':
return ( return (
<video className={'asset-preview'}> <video className={'asset-preview'}>
<source src={directSourceLink} type={contentType}/> <source src={directSourceLink} type={contentType} />
</video> </video>
); );
default: default:

View file

@ -7,9 +7,9 @@ class ErrorPage extends React.Component {
const { error } = this.props; const { error } = this.props;
return ( return (
<div> <div>
<NavBar/> <NavBar />
<div className="row row--padded"> <div className='row row--padded'>
<p>{error}</p> <p>{error}</p>
</div> </div>
</div> </div>
); );
@ -18,6 +18,6 @@ class ErrorPage extends React.Component {
ErrorPage.propTypes = { ErrorPage.propTypes = {
error: PropTypes.string.isRequired, error: PropTypes.string.isRequired,
} };
export default ErrorPage; export default ErrorPage;

View file

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

View file

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

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,4 +1,5 @@
import React from 'react'; import React from 'react';
import SEO from 'components/SEO';
import NavBar from 'containers/NavBar'; import NavBar from 'containers/NavBar';
import ErrorPage from 'components/ErrorPage'; import ErrorPage from 'components/ErrorPage';
import AssetTitle from 'components/AssetTitle'; import AssetTitle from 'components/AssetTitle';
@ -9,29 +10,31 @@ class ShowAssetDetails extends React.Component {
render () { render () {
const { asset } = this.props; const { asset } = this.props;
if (asset) { if (asset) {
const { name } = asset.claimData;
return ( return (
<div> <div>
<NavBar/> <SEO pageTitle={`${name} - details`} asset={asset} />
<div className="row row--tall row--padded"> <NavBar />
<div className="column column--10"> <div className='row row--tall row--padded'>
<div className='column column--10'>
<AssetTitle /> <AssetTitle />
</div> </div>
<div className="column column--5 column--sml-10 align-content-top"> <div className='column column--5 column--sml-10 align-content-top'>
<div className="row row--padded"> <div className='row row--padded'>
<AssetDisplay /> <AssetDisplay />
</div> </div>
</div><div className="column column--5 column--sml-10 align-content-top"> </div><div className='column column--5 column--sml-10 align-content-top'>
<div className="row row--padded"> <div className='row row--padded'>
<AssetInfo /> <AssetInfo />
</div>
</div> </div>
</div> </div>
</div>
} }
</div> </div>
); );
}; };
return ( return (
<ErrorPage error={'loading asset data...'}/> <ErrorPage error={'loading asset data...'} />
); );
} }
}; };

View file

@ -1,19 +1,26 @@
import React from 'react'; import React from 'react';
import SEO from 'components/SEO';
import { Link } from 'react-router-dom'; import { Link } from 'react-router-dom';
import AssetDisplay from 'components/AssetDisplay'; import AssetDisplay from 'components/AssetDisplay';
class ShowLite extends React.Component { class ShowLite extends React.Component {
render () { render () {
const { asset } = this.props; const { asset } = this.props;
return ( if (asset) {
<div className="row row--tall flex-container--column flex-container--center-center"> const { name, claimId } = asset.claimData;
{ (asset) && return (
<div> <div className='row row--tall flex-container--column flex-container--center-center'>
<AssetDisplay /> <SEO pageTitle={name} asset={asset} />
<Link id="asset-boilerpate" className="link--primary fine-print" to={`/${asset.claimId}/${asset.name}`}>hosted via Spee.ch</Link> <div>
<AssetDisplay />
<Link id='asset-boilerpate' className='link--primary fine-print' to={`/${claimId}/${name}`}>hosted
via Spee.ch</Link>
</div>
</div> </div>
} );
</div> }
return (
<p>loading asset data...</p>
); );
} }
}; };

View file

@ -1,4 +1,5 @@
import React from 'react'; import React from 'react';
import SEO from 'components/SEO';
import ErrorPage from 'components/ErrorPage'; import ErrorPage from 'components/ErrorPage';
import NavBar from 'containers/NavBar'; import NavBar from 'containers/NavBar';
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay'; import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
@ -10,14 +11,15 @@ class ShowChannel extends React.Component {
const { name, longId, shortId } = channel; const { name, longId, shortId } = channel;
return ( return (
<div> <div>
<NavBar/> <SEO pageTitle={name} channel={channel} />
<div className="row row--tall row--padded"> <NavBar />
<div className="column column--10"> <div className='row row--tall row--padded'>
<h2>channel name: {name || 'loading...'}</h2> <div className='column column--10'>
<p className={'fine-print'}>full channel id: {longId || 'loading...'}</p> <h2>channel name: {name}</h2>
<p className={'fine-print'}>short channel id: {shortId || 'loading...'}</p> <p className={'fine-print'}>full channel id: {longId}</p>
<p className={'fine-print'}>short channel id: {shortId}</p>
</div> </div>
<div className="column column--10"> <div className='column column--10'>
<ChannelClaimsDisplay /> <ChannelClaimsDisplay />
</div> </div>
</div> </div>
@ -25,7 +27,7 @@ class ShowChannel extends React.Component {
); );
}; };
return ( return (
<ErrorPage error={'loading channel data...'}/> <ErrorPage error={'loading channel data...'} />
); );
} }
}; };

View file

@ -1,6 +1,7 @@
// request actions // request actions
export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI'; export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI';
export const REQUEST_UPDATE_ERROR = 'REQUEST_UPDATE_ERROR'; export const REQUEST_ERROR = 'REQUEST_ERROR';
export const REQUEST_UPDATE = 'REQUEST_UPDATE';
export const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW'; export const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW';
export const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW'; export const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW';
export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD'; export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';

View file

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

View file

@ -1,10 +1,11 @@
import React from 'react'; import React from 'react';
import { withRouter } from 'react-router-dom'; import { withRouter } from 'react-router-dom';
import SEO from 'components/SEO';
import NavBar from 'containers/NavBar'; import NavBar from 'containers/NavBar';
import ChannelLoginForm from 'containers/ChannelLoginForm'; import ChannelLoginForm from 'containers/ChannelLoginForm';
import ChannelCreateForm from 'containers/ChannelCreateForm'; import ChannelCreateForm from 'containers/ChannelCreateForm';
class PublishPage extends React.Component { class LoginPage extends React.Component {
componentWillReceiveProps (newProps) { componentWillReceiveProps (newProps) {
// re-route the user to the homepage if the user is logged in // re-route the user to the homepage if the user is logged in
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) { if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
@ -15,24 +16,25 @@ class PublishPage extends React.Component {
render () { render () {
neb-b commented 2018-02-23 21:25:05 +01:00 (Migrated from github.com)
Review

You can move all of these utility functions into the SEO component. Just pass in the minimum values needed and create the actual title/link/tags inside the component.

You can move all of these utility functions into the `SEO` component. Just pass in the minimum values needed and create the actual title/link/tags inside the component.
neb-b commented 2018-02-23 21:30:19 +01:00 (Migrated from github.com)
Review

Then you don't need to add the same three lines to every page component.
<SEO title="Login" link="login" />
or
<SEO title="Channel" link="channel" channel={channel} />

Then in the component you can check if channel or asset was passed in and build the meta tags accordingly.

if (!props.asset || !props.channel) {
    metaTags = createBasicMetaTags();
}
Then you don't need to add the same three lines to every page component. `<SEO title="Login" link="login" />` or `<SEO title="Channel" link="channel" channel={channel} />` Then in the component you can check if `channel` or `asset` was passed in and build the meta tags accordingly. ``` if (!props.asset || !props.channel) { metaTags = createBasicMetaTags(); } ```
return ( return (
<div> <div>
<NavBar/> <SEO pageTitle={'Login'} pageUri={'login'} />
<div className="row row--padded"> <NavBar />
<div className="column column--5 column--med-10 align-content-top"> <div className='row row--padded'>
<div className="column column--8 column--med-10"> <div className='column column--5 column--med-10 align-content-top'>
<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 className='column column--8 column--med-10'>
<p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're <a className='link--primary' target='_blank' href='/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8'>documenting important events</a>, or making a public repository for <a className='link--primary' target='_blank' href='/@catGifs'>cat gifs</a> (password: '1234'), try creating a channel for it!</p>
</div>
</div><div className='column column--5 column--med-10 align-content-top'>
<div className='column column--8 column--med-10'>
<h3 className='h3--no-bottom'>Log in to an existing channel:</h3>
<ChannelLoginForm />
<h3 className='h3--no-bottom'>Create a brand new channel:</h3>
<ChannelCreateForm />
</div> </div>
</div><div 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>
</div>
</div> </div>
); );
} }
}; };
export default withRouter(PublishPage); export default withRouter(LoginPage);

View file

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

View file

@ -1,5 +1,5 @@
import { connect } from 'react-redux'; import { connect } from 'react-redux';
import { handleShowPageUri } from 'actions/show'; import { onHandleShowPageUri } from 'actions/show';
import View from './view'; import View from './view';
const mapStateToProps = ({ show }) => { const mapStateToProps = ({ show }) => {
@ -10,7 +10,7 @@ const mapStateToProps = ({ show }) => {
}; };
const mapDispatchToProps = { const mapDispatchToProps = {
handleShowPageUri, onHandleShowPageUri,
}; };
export default connect(mapStateToProps, mapDispatchToProps)(View); export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -8,18 +8,18 @@ import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types
class ShowPage extends React.Component { class ShowPage extends React.Component {
componentDidMount () { componentDidMount () {
this.props.handleShowPageUri(this.props.match.params); this.props.onHandleShowPageUri(this.props.match.params);
} }
componentWillReceiveProps (nextProps) { componentWillReceiveProps (nextProps) {
if (nextProps.match.params !== this.props.match.params) { if (nextProps.match.params !== this.props.match.params) {
this.props.handleShowPageUri(nextProps.match.params); this.props.onHandleShowPageUri(nextProps.match.params);
} }
} }
render () { render () {
const { error, requestType } = this.props; const { error, requestType } = this.props;
if (error) { if (error) {
return ( return (
<ErrorPage error={error}/> <ErrorPage error={error} />
); );
} }
switch (requestType) { switch (requestType) {

View file

@ -1,24 +0,0 @@
import React from 'react';
import { render } from 'react-dom';
import { createStore, applyMiddleware, compose } from 'redux';
import Reducer from 'reducers';
import createSagaMiddleware from 'redux-saga';
import rootSaga from 'sagas';
import Root from './root';
const sagaMiddleware = createSagaMiddleware();
const middleware = applyMiddleware(sagaMiddleware);
const enhancer = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;
let store = createStore(
Reducer,
enhancer,
);
sagaMiddleware.run(rootSaga);
render(
<Root store={store} />,
document.getElementById('react-app')
);

View file

@ -19,14 +19,13 @@ const initialState = {
export default function (state = initialState, action) { export default function (state = initialState, action) {
switch (action.type) { switch (action.type) {
// handle request // handle request
case actions.REQUEST_UPDATE_ERROR: case actions.REQUEST_ERROR:
return Object.assign({}, state, { return Object.assign({}, state, {
request: Object.assign({}, state.request, { request: Object.assign({}, state.request, {
error: action.data, error: action.data,
}), }),
}); });
case actions.CHANNEL_REQUEST_NEW: case actions.REQUEST_UPDATE:
case actions.ASSET_REQUEST_NEW:
return Object.assign({}, state, { return Object.assign({}, state, {
request: Object.assign({}, state.request, { request: Object.assign({}, state.request, {
type: action.data.requestType, type: action.data.requestType,

View file

@ -1,34 +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 GAListener from 'components/GAListener';
import PublishPage from 'components/PublishPage';
import AboutPage from 'components/AboutPage';
import LoginPage from 'containers/LoginPage';
import ShowPage from 'containers/ShowPage';
import FourOhFourPage from 'components/FourOhFourPage';
const Root = ({ store }) => (
<Provider store={store}>
<BrowserRouter>
<GAListener>
<Switch>
<Route exact path="/" component={PublishPage} />
<Route exact path="/about" component={AboutPage} />
<Route exact path="/login" component={LoginPage} />
<Route exact path="/:identifier/:claim" component={ShowPage} />
<Route exact path="/:claim" component={ShowPage} />
<Route component={FourOhFourPage} />
</Switch>
</GAListener>
</BrowserRouter>
</Provider>
);
Root.propTypes = {
store: PropTypes.object.isRequired,
};
export default Root;

View file

@ -4,7 +4,7 @@ import { updateFileAvailability, updateDisplayAssetError } from 'actions/show';
import { UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states'; import { UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states';
import { checkFileAvailability, triggerClaimGet } from 'api/fileApi'; import { checkFileAvailability, triggerClaimGet } from 'api/fileApi';
function* retrieveFile (action) { function * retrieveFile (action) {
const name = action.data.name; const name = action.data.name;
const claimId = action.data.claimId; const claimId = action.data.claimId;
// see if the file is available // see if the file is available
@ -28,6 +28,6 @@ function* retrieveFile (action) {
yield put(updateFileAvailability(AVAILABLE)); yield put(updateFileAvailability(AVAILABLE));
}; };
export function* watchFileIsRequested () { export function * watchFileIsRequested () {
yield takeLatest(actions.FILE_REQUESTED, retrieveFile); yield takeLatest(actions.FILE_REQUESTED, retrieveFile);
}; };

View file

@ -4,7 +4,7 @@ import { watchNewAssetRequest } from './show_asset';
import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel'; import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';
import { watchFileIsRequested } from './file'; import { watchFileIsRequested } from './file';
export default function* rootSaga () { export default function * rootSaga () {
yield all([ yield all([
watchHandleShowPageUri(), watchHandleShowPageUri(),
watchNewAssetRequest(), watchNewAssetRequest(),

View file

@ -1,14 +1,16 @@
import { call, put, select, takeLatest } from 'redux-saga/effects'; import { call, put, select, takeLatest } from 'redux-saga/effects';
import * as actions from 'constants/show_action_types'; import * as actions from 'constants/show_action_types';
import { addRequestToRequestList, onRequestError, addAssetToAssetList } from 'actions/show'; import { addRequestToRequestList, onRequestError, onRequestUpdate, addAssetToAssetList } from 'actions/show';
import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi'; import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi';
import { selectShowState } from 'selectors/show'; import { selectShowState } from 'selectors/show';
function* newAssetRequest (action) { export function * newAssetRequest (action) {
const { requestId, name, modifier } = action.data; const { requestType, requestId, name, modifier } = action.data;
const state = yield select(selectShowState); // put an action to update the request in redux
yield put(onRequestUpdate(requestType, requestId));
// is this an existing request? // is this an existing request?
// If this uri is in the request list, it's already been fetched // If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
if (state.requestList[requestId]) { if (state.requestList[requestId]) {
console.log('that request already exists in the request list!'); console.log('that request already exists in the request list!');
return null; return null;
@ -52,6 +54,6 @@ function* newAssetRequest (action) {
yield put(onRequestError(null)); yield put(onRequestError(null));
}; };
export function* watchNewAssetRequest () { export function * watchNewAssetRequest () {
yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest); yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest);
}; };

View file

@ -1,14 +1,16 @@
import {call, put, select, takeLatest} from 'redux-saga/effects'; import {call, put, select, takeLatest} from 'redux-saga/effects';
import * as actions from 'constants/show_action_types'; import * as actions from 'constants/show_action_types';
import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, updateChannelClaims } from 'actions/show'; import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from 'actions/show';
import { getChannelClaims, getChannelData } from 'api/channelApi'; import { getChannelClaims, getChannelData } from 'api/channelApi';
import { selectShowState } from 'selectors/show'; import { selectShowState } from 'selectors/show';
function* getNewChannelAndUpdateChannelList (action) { export function * newChannelRequest (action) {
const { requestId, channelName, channelId } = action.data; const { requestType, requestId, channelName, channelId } = action.data;
const state = yield select(selectShowState); // put an action to update the request in redux
yield put(onRequestUpdate(requestType, requestId));
// is this an existing request? // is this an existing request?
// If this uri is in the request list, it's already been fetched // If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
if (state.requestList[requestId]) { if (state.requestList[requestId]) {
console.log('that request already exists in the request list!'); console.log('that request already exists in the request list!');
return null; return null;
@ -44,11 +46,11 @@ function* getNewChannelAndUpdateChannelList (action) {
yield put(onRequestError(null)); yield put(onRequestError(null));
} }
export function* watchNewChannelRequest () { export function * watchNewChannelRequest () {
yield takeLatest(actions.CHANNEL_REQUEST_NEW, getNewChannelAndUpdateChannelList); yield takeLatest(actions.CHANNEL_REQUEST_NEW, newChannelRequest);
}; };
function* getNewClaimsAndUpdateChannel (action) { function * getNewClaimsAndUpdateChannel (action) {
const { channelKey, name, longId, page } = action.data; const { channelKey, name, longId, page } = action.data;
let claimsData; let claimsData;
try { try {
@ -59,6 +61,6 @@ function* getNewClaimsAndUpdateChannel (action) {
yield put(updateChannelClaims(channelKey, claimsData)); yield put(updateChannelClaims(channelKey, claimsData));
} }
export function* watchUpdateChannelClaims () { export function * watchUpdateChannelClaims () {
yield takeLatest(actions.CHANNEL_CLAIMS_UPDATE_ASYNC, getNewClaimsAndUpdateChannel); yield takeLatest(actions.CHANNEL_CLAIMS_UPDATE_ASYNC, getNewClaimsAndUpdateChannel);
} }

View file

@ -1,9 +1,11 @@
import { call, put, takeLatest } from 'redux-saga/effects'; import { call, put, takeLatest } from 'redux-saga/effects';
import * as actions from 'constants/show_action_types'; import * as actions from 'constants/show_action_types';
import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show'; import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show';
import { newAssetRequest } from 'sagas/show_asset';
import { newChannelRequest } from 'sagas/show_channel';
import lbryUri from 'utils/lbryUri'; import lbryUri from 'utils/lbryUri';
function* parseAndUpdateIdentifierAndClaim (modifier, claim) { function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
console.log('parseAndUpdateIdentifierAndClaim'); console.log('parseAndUpdateIdentifierAndClaim');
// this is a request for an asset // this is a request for an asset
// claim will be an asset claim // claim will be an asset claim
@ -17,11 +19,11 @@ function* parseAndUpdateIdentifierAndClaim (modifier, claim) {
} }
// trigger an new action to update the store // trigger an new action to update the store
if (isChannel) { if (isChannel) {
return yield put(onNewAssetRequest(claimName, null, channelName, channelClaimId, extension)); return yield call(newAssetRequest, onNewAssetRequest(claimName, null, channelName, channelClaimId, extension));
}; };
yield put(onNewAssetRequest(claimName, claimId, null, null, extension)); yield call(newAssetRequest, onNewAssetRequest(claimName, claimId, null, null, extension));
} }
function* parseAndUpdateClaimOnly (claim) { function * parseAndUpdateClaimOnly (claim) {
console.log('parseAndUpdateIdentifierAndClaim'); console.log('parseAndUpdateIdentifierAndClaim');
// this could be a request for an asset or a channel page // this could be a request for an asset or a channel page
// claim could be an asset claim or a channel claim // claim could be an asset claim or a channel claim
@ -34,7 +36,7 @@ function* parseAndUpdateClaimOnly (claim) {
// trigger an new action to update the store // trigger an new action to update the store
// return early if this request is for a channel // return early if this request is for a channel
if (isChannel) { if (isChannel) {
return yield put(onNewChannelRequest(channelName, channelClaimId)); return yield call(newChannelRequest, onNewChannelRequest(channelName, channelClaimId));
} }
// if not for a channel, parse the claim request // if not for a channel, parse the claim request
let claimName, extension; let claimName, extension;
@ -43,10 +45,10 @@ function* parseAndUpdateClaimOnly (claim) {
} catch (error) { } catch (error) {
return yield put(onRequestError(error.message)); return yield put(onRequestError(error.message));
} }
yield put(onNewAssetRequest(claimName, null, null, null, extension)); yield call(newAssetRequest, onNewAssetRequest(claimName, null, null, null, extension));
} }
function* handleShowPageUri (action) { export function * handleShowPageUri (action) {
console.log('handleShowPageUri'); console.log('handleShowPageUri');
const { identifier, claim } = action.data; const { identifier, claim } = action.data;
if (identifier) { if (identifier) {
@ -55,6 +57,6 @@ function* handleShowPageUri (action) {
yield call(parseAndUpdateClaimOnly, claim); yield call(parseAndUpdateClaimOnly, claim);
}; };
export function* watchHandleShowPageUri () { export function * watchHandleShowPageUri () {
yield takeLatest(actions.HANDLE_SHOW_URI, handleShowPageUri); yield takeLatest(actions.HANDLE_SHOW_URI, handleShowPageUri);
}; };

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.'); throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
} }
}, },
} };

View file

@ -8,7 +8,7 @@ module.exports = {
'([^:$#/]*)' + // value (stops at the first separator or end) '([^:$#/]*)' + // value (stops at the first separator or end)
'([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path 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) .exec(identifier)
.map(match => match || null); .map(match => match || null);
@ -56,7 +56,7 @@ module.exports = {
'([^:$#/.]*)' + // name (stops at the first extension) '([^:$#/.]*)' + // name (stops at the first extension)
'([:$#.]?)([^/]*)' // extension separator, extension (stops at the first path separator or end) '([:$#.]?)([^/]*)' // 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) .exec(name)
.map(match => match || null); .map(match => match || null);

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 * Parses the JSON returned by a network request
* *

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

View file

@ -1,13 +1,13 @@
const { sendGAServeEvent } = require('../helpers/googleAnalytics'); const { sendGAServeEvent } = require('../helpers/googleAnalytics');
const { determineResponseType, flipClaimNameAndIdForBackwardsCompatibility, logRequestData, getClaimIdAndServeAsset } = require('../helpers/serveHelpers.js'); const { determineResponseType, flipClaimNameAndIdForBackwardsCompatibility, logRequestData, getClaimIdAndServeAsset } = require('../helpers/serveHelpers.js');
const lbryUri = require('../helpers/lbryUri.js'); const lbryUri = require('../helpers/lbryUri.js');
const handleShowRender = require('../helpers/handleShowRender.jsx');
const SERVE = 'SERVE'; const SERVE = 'SERVE';
module.exports = (app) => { module.exports = (app) => {
// route to serve a specific asset using the channel or claim id // route to serve a specific asset using the channel or claim id
app.get('/:identifier/:claim', ({ headers, ip, originalUrl, params }, res) => { app.get('/:identifier/:claim', (req, res) => {
const { headers, ip, originalUrl, params } = req;
// decide if this is a show request // decide if this is a show request
let hasFileExtension; let hasFileExtension;
try { try {
@ -17,7 +17,7 @@ module.exports = (app) => {
} }
let responseType = determineResponseType(hasFileExtension, headers); let responseType = determineResponseType(hasFileExtension, headers);
if (responseType !== SERVE) { if (responseType !== SERVE) {
return res.status(200).render('index'); return handleShowRender(req, res);
} }
// handle serve request // handle serve request
// send google analytics // send google analytics
@ -45,7 +45,8 @@ module.exports = (app) => {
getClaimIdAndServeAsset(channelName, channelClaimId, claimName, claimId, originalUrl, ip, res); getClaimIdAndServeAsset(channelName, channelClaimId, claimName, claimId, originalUrl, ip, res);
}); });
// route to serve the winning asset at a claim or a channel page // route to serve the winning asset at a claim or a channel page
app.get('/:claim', ({ headers, ip, originalUrl, params, query }, res) => { app.get('/:claim', (req, res) => {
const { headers, ip, originalUrl, params } = req;
// decide if this is a show request // decide if this is a show request
let hasFileExtension; let hasFileExtension;
try { try {
@ -55,7 +56,7 @@ module.exports = (app) => {
} }
let responseType = determineResponseType(hasFileExtension, headers); let responseType = determineResponseType(hasFileExtension, headers);
if (responseType !== SERVE) { if (responseType !== SERVE) {
return res.status(200).render('index'); return handleShowRender(req, res);
} }
// handle serve request // handle serve request
// send google analytics // send google analytics

View file

@ -1,4 +0,0 @@
<div class="row row--tall flex-container--column flex-container--center-center">
<h3>404: Not Found</h3>
<p>That page does not exist. Return <a class="link--primary" href="/">home</a>.</p>
</div>

View file

@ -1,10 +0,0 @@
<div class="row row--tall flex-container--column">
<div id="react-app" class="row row--tall flex-container--column">
<div class="row row--padded row--tall flex-container--column flex-container--center-center">
<p>loading...</p>
{{> progressBar}}
</div>
</div>
</div>
<script src="/bundle/bundle.js"></script>

View file

@ -1,19 +0,0 @@
<!DOCTYPE html>
<html lang="en" prefix="og: http://ogp.me/ns# fb: http://ogp.me/ns/fb#">
<head>
{{ placeCommonHeaderTags }}
<meta name="twitter:card" content="summary" />
<meta name="twitter:site" content="@spee_ch" />
<meta property="og:title" content="Spee.ch" />
<meta property="og:site_name" content="Spee.ch" />
<meta property="og:type" content="website" />
<meta property="og:image" content="https://spee.ch/assets/img/Speech_Logo_Main@OG-02.jpg" />
<meta property="og:url" content="http://spee.ch/" />
<meta property="og:description" content="Open-source, decentralized image and video sharing." />
<!--google font-->
<link href="https://fonts.googleapis.com/css?family=Roboto:300" rel="stylesheet">
</head>
<body id="main-body">
{{{ body }}}
</body>
</html>

View file

@ -1,14 +1,14 @@
const Path = require('path'); const Path = require('path');
const REACT_ROOT = Path.resolve(__dirname, 'react/'); const REACT_ROOT = Path.resolve(__dirname, 'react/');
module.exports = { module.exports = {
entry : ['babel-polyfill', 'whatwg-fetch', './react/index.js'], target: 'web',
entry : ['babel-polyfill', 'whatwg-fetch', './react/client.js'],
output: { output: {
path : Path.join(__dirname, '/public/bundle/'), path : Path.join(__dirname, 'public/bundle/'),
filename: 'bundle.js', publicPath: 'public/bundle/',
filename : 'bundle.js',
}, },
watch : true,
module: { module: {
loaders: [ loaders: [
{ {

13
webpack.dev.js Normal file
View file

@ -0,0 +1,13 @@
const serverBaseConfig = require('./webpack.server.common.js');
const clientBaseConfig = require('./webpack.client.common.js');
const merge = require('webpack-merge');
const devBuildConfig = {
watch : true,
devtool: 'inline-source-map',
};
module.exports = [
merge(serverBaseConfig, devBuildConfig),
merge(clientBaseConfig, devBuildConfig),
];

22
webpack.prod.js Normal file
View file

@ -0,0 +1,22 @@
const webpack = require('webpack');
const merge = require('webpack-merge');
const UglifyJSPlugin = require('uglifyjs-webpack-plugin');
const serverBaseConfig = require('./webpack.server.common.js');
const clientBaseConfig = require('./webpack.client.common.js');
const productionBuildConfig = {
devtool: 'source-map',
plugins: [
new UglifyJSPlugin({
sourceMap: true,
}),
new webpack.DefinePlugin({
'process.env.NODE_ENV': JSON.stringify('production'),
}),
],
};
module.exports = [
merge(serverBaseConfig, productionBuildConfig),
merge(clientBaseConfig, productionBuildConfig),
];

41
webpack.server.common.js Normal file
View file

@ -0,0 +1,41 @@
const Path = require('path');
const nodeExternals = require('webpack-node-externals');
const REACT_ROOT = Path.resolve(__dirname, 'react/');
module.exports = {
target: 'node',
node : {
__dirname: false,
},
externals: [nodeExternals()],
entry : ['babel-polyfill', 'whatwg-fetch', './index.js'],
output : {
path : Path.join(__dirname, '/'),
publicPath: '/',
filename : 'server.js',
},
module: {
rules: [
{
test : /.jsx?$/,
exclude: /node_modules/,
loader : 'babel-loader',
options: {
presets: ['es2015', 'react', 'stage-2'],
},
},
{
test : /.css$/,
loader: 'css-loader',
},
],
},
resolve: {
modules: [
REACT_ROOT,
'node_modules',
__dirname,
],
extensions: ['.js', '.json', '.jsx', '.css'],
},
};