fixed merge conflicts
This commit is contained in:
commit
6bc89527ad
115 changed files with 2422 additions and 27317 deletions
|
@ -1,9 +1,10 @@
|
|||
{
|
||||
"extends": "standard",
|
||||
"extends": ["standard", "standard-jsx"],
|
||||
"env": {
|
||||
"es6": true,
|
||||
"jest": true,
|
||||
"node": true
|
||||
"node": true,
|
||||
"browser": true
|
||||
},
|
||||
"globals": {
|
||||
"GENTLY": true
|
||||
|
|
5
.gitignore
vendored
5
.gitignore
vendored
|
@ -1,4 +1,7 @@
|
|||
node_modules
|
||||
.idea
|
||||
config/sequelizeCliConfig.js
|
||||
config/speechConfig.js
|
||||
config/speechConfig.js
|
||||
public/bundle
|
||||
server.js
|
||||
webpack.config.js
|
||||
|
|
15
LICENSE
Normal file
15
LICENSE
Normal file
|
@ -0,0 +1,15 @@
|
|||
The MIT License (MIT)
|
||||
|
||||
Copyright (c) 2017-2018 LBRY Inc
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the
|
||||
"Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish,
|
||||
distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the
|
||||
following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
|
||||
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
|
||||
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
30
README.md
30
README.md
|
@ -1,5 +1,5 @@
|
|||
# spee.ch
|
||||
spee.ch is a single-serving site that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
|
||||
# Spee.ch
|
||||
Spee.ch is a web app that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
|
||||
|
||||
## how to run this repository locally
|
||||
* start mysql
|
||||
|
@ -16,9 +16,9 @@ spee.ch is a single-serving site that reads and publishes images and videos to a
|
|||
* run `npm install`
|
||||
* create your `speechConfig.js` file
|
||||
* copy `speechConfig.js.example` and name it `speechConfig.js`
|
||||
* replace the `null` values in the config file with the appropriate values for your environement
|
||||
* to start the server, from your command line run `node speech.js`
|
||||
* To run hot, use `nodemon` instead of `node`
|
||||
* replace the `null` values in the config file with the appropriate values for your environment
|
||||
* build the app by running `npm run build-prod`
|
||||
* to start the server, run `npm run start`
|
||||
* visit [localhost:3000](http://localhost:3000)
|
||||
|
||||
## Tests
|
||||
|
@ -29,20 +29,20 @@ spee.ch is a single-serving site that reads and publishes images and videos to a
|
|||
## API
|
||||
|
||||
#### GET
|
||||
* /api/claim-resolve/:name/:claimId
|
||||
* example: `curl https://spee.ch/api/claim-resolve/doitlive/xyz`
|
||||
* /api/claim-list/:name
|
||||
* example: `curl https://spee.ch/api/claim-list/doitlive`
|
||||
* /api/claim-is-available/:name (
|
||||
* /api/claim/resolve/:name/:claimId
|
||||
* example: `curl https://spee.ch/api/claim/resolve/doitlive/xyz`
|
||||
* /api/claim/list/:name
|
||||
* example: `curl https://spee.ch/api/claim/list/doitlive`
|
||||
* /api/claim/availability/:name (
|
||||
* returns `true`/`false` for whether a name is available through spee.ch
|
||||
* example: `curl https://spee.ch/api/claim-is-available/doitlive`
|
||||
* /api/channel-is-available/:name (
|
||||
* example: `curl https://spee.ch/api/claim/availability/doitlive`
|
||||
* /api/channel/availability/:name (
|
||||
* returns `true`/`false` for whether a channel is available through spee.ch
|
||||
* example: `curl https://spee.ch/api/channel-is-available/@CoolChannel`
|
||||
* example: `curl https://spee.ch/api/channel/availability/@CoolChannel`
|
||||
|
||||
#### POST
|
||||
* /api/claim-publish
|
||||
* example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim-publish`
|
||||
* /api/claim/publish
|
||||
* example: `curl -X POST -F 'name=MyPictureName' -F 'file=@/path/to/myPicture.jpeg' https://spee.ch/api/claim/publish`
|
||||
* Parameters:
|
||||
* `name`
|
||||
* `file` (must be type .mp4, .jpeg, .jpg, .gif, or .png)
|
||||
|
|
|
@ -7,31 +7,31 @@ module.exports = {
|
|||
const userName = channelName.substring(1);
|
||||
logger.debug(`authenticateChannelCredentials > channelName: ${channelName} username: ${userName} pass: ${userPassword}`);
|
||||
db.User
|
||||
.findOne({where: { userName }})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
logger.debug('no user found');
|
||||
.findOne({where: { userName }})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
logger.debug('no user found');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
return user.comparePassword(userPassword, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('comparePassword error:', passwordErr);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
return user.comparePassword(userPassword, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('comparePassword error:', passwordErr);
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
if (!isMatch) {
|
||||
logger.debug('incorrect password');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
logger.debug('...password was a match...');
|
||||
resolve(true);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
if (!isMatch) {
|
||||
logger.debug('incorrect password');
|
||||
resolve(false);
|
||||
return;
|
||||
}
|
||||
logger.debug('...password was a match...');
|
||||
resolve(true);
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
authenticateIfNoUserToken (channelName, channelPassword, user) {
|
||||
|
|
|
@ -24,11 +24,13 @@ module.exports = {
|
|||
},
|
||||
site: {
|
||||
title: 'Spee.ch',
|
||||
name : 'Spee.ch',
|
||||
host : 'https://spee.ch',
|
||||
description: 'Open-source, decentralized image and video sharing.'
|
||||
},
|
||||
publish: {
|
||||
thumbnailChannel: '@channelName:channelId', // create a channel to use for thumbnail images
|
||||
},
|
||||
}
|
||||
claim: {
|
||||
defaultTitle : 'Spee.ch',
|
||||
defaultThumbnail : 'https://spee.ch/assets/img/video_thumb_default.png',
|
||||
|
|
|
@ -10,80 +10,80 @@ module.exports = {
|
|||
let publishResults, certificateId, channelName;
|
||||
// publish the file
|
||||
return lbryApi.publishClaim(publishParams)
|
||||
.then(tx => {
|
||||
logger.info(`Successfully published ${publishParams.name} ${fileName}`, tx);
|
||||
publishResults = tx;
|
||||
// get the channel information
|
||||
if (publishParams.channel_name) {
|
||||
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
||||
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
|
||||
} else {
|
||||
logger.debug('this claim was not published in a channel');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.then(channel => {
|
||||
.then(tx => {
|
||||
logger.info(`Successfully published ${publishParams.name} ${fileName}`, tx);
|
||||
publishResults = tx;
|
||||
// get the channel information
|
||||
if (publishParams.channel_name) {
|
||||
logger.debug(`this claim was published in channel: ${publishParams.channel_name}`);
|
||||
return db.Channel.findOne({where: {channelName: publishParams.channel_name}});
|
||||
} else {
|
||||
logger.debug('this claim was not published in a channel');
|
||||
return null;
|
||||
}
|
||||
})
|
||||
.then(channel => {
|
||||
// set channel information
|
||||
certificateId = null;
|
||||
channelName = null;
|
||||
if (channel) {
|
||||
certificateId = channel.channelClaimId;
|
||||
channelName = channel.channelName;
|
||||
}
|
||||
logger.debug(`certificateId: ${certificateId}`);
|
||||
})
|
||||
.then(() => {
|
||||
certificateId = null;
|
||||
channelName = null;
|
||||
if (channel) {
|
||||
certificateId = channel.channelClaimId;
|
||||
channelName = channel.channelName;
|
||||
}
|
||||
logger.debug(`certificateId: ${certificateId}`);
|
||||
})
|
||||
.then(() => {
|
||||
// create the File record
|
||||
const fileRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
fileName,
|
||||
filePath : publishParams.file_path,
|
||||
fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
};
|
||||
// create the Claim record
|
||||
const claimRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
thumbnail : publishParams.metadata.thumbnail,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
contentType: fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
amount : publishParams.bid,
|
||||
certificateId,
|
||||
channelName,
|
||||
};
|
||||
// upsert criteria
|
||||
const upsertCriteria = {
|
||||
name : publishParams.name,
|
||||
claimId: publishResults.claim_id,
|
||||
};
|
||||
// upsert the records
|
||||
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim')]);
|
||||
})
|
||||
.then(([file, claim]) => {
|
||||
logger.debug('File and Claim records successfully created');
|
||||
return Promise.all([file.setClaim(claim), claim.setFile(file)]);
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('File and Claim records successfully associated');
|
||||
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('PUBLISH ERROR', error);
|
||||
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
|
||||
reject(error);
|
||||
});
|
||||
const fileRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
fileName,
|
||||
filePath : publishParams.file_path,
|
||||
fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
};
|
||||
// create the Claim record
|
||||
const claimRecord = {
|
||||
name : publishParams.name,
|
||||
claimId : publishResults.claim_id,
|
||||
title : publishParams.metadata.title,
|
||||
description: publishParams.metadata.description,
|
||||
address : publishParams.claim_address,
|
||||
thumbnail : publishParams.metadata.thumbnail,
|
||||
outpoint : `${publishResults.txid}:${publishResults.nout}`,
|
||||
height : 0,
|
||||
contentType: fileType,
|
||||
nsfw : publishParams.metadata.nsfw,
|
||||
amount : publishParams.bid,
|
||||
certificateId,
|
||||
channelName,
|
||||
};
|
||||
// upsert criteria
|
||||
const upsertCriteria = {
|
||||
name : publishParams.name,
|
||||
claimId: publishResults.claim_id,
|
||||
};
|
||||
// upsert the records
|
||||
return Promise.all([db.upsert(db.File, fileRecord, upsertCriteria, 'File'), db.upsert(db.Claim, claimRecord, upsertCriteria, 'Claim')]);
|
||||
})
|
||||
.then(([file, claim]) => {
|
||||
logger.debug('File and Claim records successfully created');
|
||||
return Promise.all([file.setClaim(claim), claim.setFile(file)]);
|
||||
})
|
||||
.then(() => {
|
||||
logger.debug('File and Claim records successfully associated');
|
||||
resolve(publishResults); // resolve the promise with the result from lbryApi.publishClaim;
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('PUBLISH ERROR', error);
|
||||
publishHelpers.deleteTemporaryFile(publishParams.file_path); // delete the local file
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
},
|
||||
checkClaimNameAvailability (name) {
|
||||
|
|
|
@ -6,11 +6,11 @@ module.exports = function () {
|
|||
|
||||
for (let configCategoryKey in config) {
|
||||
if (config.hasOwnProperty(configCategoryKey)) {
|
||||
// get the final variables for each config category
|
||||
// get the final variables for each config category
|
||||
const configVariables = config[configCategoryKey];
|
||||
for (let configVarKey in configVariables) {
|
||||
if (configVariables.hasOwnProperty(configVarKey)) {
|
||||
// print each variable
|
||||
// print each variable
|
||||
logger.debug(`CONFIG CHECK: ${configCategoryKey}.${configVarKey} === ${configVariables[configVarKey]}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,37 +1,30 @@
|
|||
const logger = require('winston');
|
||||
|
||||
module.exports = {
|
||||
returnErrorMessageAndStatus: function (error) {
|
||||
let status, message;
|
||||
// check for daemon being turned off
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
status = 200;
|
||||
message = 'Connection refused. The daemon may not be running.';
|
||||
// check for thrown errors
|
||||
} else if (error.message) {
|
||||
status = 200;
|
||||
message = error.message;
|
||||
// fallback for everything else
|
||||
} else {
|
||||
status = 400;
|
||||
message = error;
|
||||
}
|
||||
return [status, message];
|
||||
},
|
||||
handleRequestError: function (originalUrl, ip, error, res) {
|
||||
logger.error(`Request Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));
|
||||
const [status, message] = module.exports.returnErrorMessageAndStatus(error);
|
||||
res
|
||||
.status(status)
|
||||
.render('requestError', module.exports.createErrorResponsePayload(status, message));
|
||||
},
|
||||
handleApiError: function (originalUrl, ip, error, res) {
|
||||
logger.error(`Api Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));
|
||||
handleErrorResponse: function (originalUrl, ip, error, res) {
|
||||
logger.error(`Error on ${originalUrl}`, module.exports.useObjectPropertiesIfNoKeys(error));
|
||||
const [status, message] = module.exports.returnErrorMessageAndStatus(error);
|
||||
res
|
||||
.status(status)
|
||||
.json(module.exports.createErrorResponsePayload(status, message));
|
||||
},
|
||||
returnErrorMessageAndStatus: function (error) {
|
||||
let status, message;
|
||||
// check for daemon being turned off
|
||||
if (error.code === 'ECONNREFUSED') {
|
||||
status = 503;
|
||||
message = 'Connection refused. The daemon may not be running.';
|
||||
// fallback for everything else
|
||||
} else {
|
||||
status = 400;
|
||||
if (error.message) {
|
||||
message = error.message;
|
||||
} else {
|
||||
message = error;
|
||||
};
|
||||
};
|
||||
return [status, message];
|
||||
},
|
||||
useObjectPropertiesIfNoKeys: function (err) {
|
||||
if (Object.keys(err).length === 0) {
|
||||
let newErrorObject = {};
|
||||
|
|
62
helpers/googleAnalytics.js
Normal file
62
helpers/googleAnalytics.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
const logger = require('winston');
|
||||
const ua = require('universal-analytics');
|
||||
const config = require('../config/speechConfig.js');
|
||||
const googleApiKey = config.analytics.googleId;
|
||||
|
||||
function createServeEventParams (headers, ip, originalUrl) {
|
||||
return {
|
||||
eventCategory : 'client requests',
|
||||
eventAction : 'serve request',
|
||||
eventLabel : originalUrl,
|
||||
ipOverride : ip,
|
||||
userAgentOverride: headers['user-agent'],
|
||||
};
|
||||
};
|
||||
|
||||
function createPublishTimingEventParams (label, startTime, endTime, ip, headers) {
|
||||
const durration = endTime - startTime;
|
||||
return {
|
||||
userTimingCategory : 'lbrynet',
|
||||
userTimingVariableName: 'publish',
|
||||
userTimingTime : durration,
|
||||
userTimingLabel : label,
|
||||
uip : ip,
|
||||
userAgentOverride : headers['user-agent'],
|
||||
};
|
||||
};
|
||||
|
||||
function sendGoogleAnalyticsEvent (ip, params) {
|
||||
const visitorId = ip.replace(/\./g, '-');
|
||||
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
|
||||
visitor.event(params, (err) => {
|
||||
if (err) {
|
||||
logger.error('Google Analytics Event Error >>', err);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function sendGoogleAnalyticsTiming (ip, params) {
|
||||
const visitorId = ip.replace(/\./g, '-');
|
||||
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
|
||||
visitor.timing(params, (err) => {
|
||||
if (err) {
|
||||
logger.error('Google Analytics Event Error >>', err);
|
||||
}
|
||||
logger.debug(`Timing event successfully sent to google analytics`);
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
sendGAServeEvent (headers, ip, originalUrl) {
|
||||
const params = createServeEventParams(headers, ip, originalUrl);
|
||||
sendGoogleAnalyticsEvent(ip, params);
|
||||
},
|
||||
sendGAAnonymousPublishTiming (headers, ip, originalUrl, startTime, endTime) {
|
||||
const params = createPublishTimingEventParams('PUBLISH_ANONYMOUS_CLAIM', startTime, endTime, ip, headers);
|
||||
sendGoogleAnalyticsTiming(ip, params);
|
||||
},
|
||||
sendGAChannelPublishTiming (headers, ip, originalUrl, startTime, endTime) {
|
||||
const params = createPublishTimingEventParams('PUBLISH_IN_CHANNEL_CLAIM', startTime, endTime, ip, headers);
|
||||
sendGoogleAnalyticsTiming(ip, params);
|
||||
},
|
||||
};
|
45
helpers/handlePageRender.jsx
Normal file
45
helpers/handlePageRender.jsx
Normal 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));
|
||||
};
|
71
helpers/handleShowRender.jsx
Normal file
71
helpers/handleShowRender.jsx
Normal 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));
|
||||
});
|
||||
};
|
|
@ -1,147 +0,0 @@
|
|||
const Handlebars = require('handlebars');
|
||||
const { site, analytics, claim: claimDefaults } = require('../config/speechConfig.js');
|
||||
|
||||
function determineOgTitle (storedTitle, defaultTitle) {
|
||||
return ifEmptyReturnOther(storedTitle, defaultTitle);
|
||||
};
|
||||
|
||||
function determineOgDescription (storedDescription, defaultDescription) {
|
||||
const length = 200;
|
||||
let description = ifEmptyReturnOther(storedDescription, defaultDescription);
|
||||
if (description.length >= length) {
|
||||
description = `${description.substring(0, length)}...`;
|
||||
};
|
||||
return description;
|
||||
};
|
||||
|
||||
function ifEmptyReturnOther (value, replacement) {
|
||||
if (value === '') {
|
||||
return replacement;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
function determineContentTypeFromFileExtension (fileExtension) {
|
||||
switch (fileExtension) {
|
||||
case 'jpeg':
|
||||
case 'jpg':
|
||||
return 'image/jpeg';
|
||||
case 'png':
|
||||
return 'image/png';
|
||||
case 'gif':
|
||||
return 'image/gif';
|
||||
case 'mp4':
|
||||
return 'video/mp4';
|
||||
default:
|
||||
return 'image/jpeg';
|
||||
}
|
||||
};
|
||||
|
||||
function determineOgThumbnailContentType (thumbnail) {
|
||||
if (thumbnail) {
|
||||
if (thumbnail.lastIndexOf('.') !== -1) {
|
||||
return determineContentTypeFromFileExtension(thumbnail.substring(thumbnail.lastIndexOf('.')));
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function createOpenGraphDataFromClaim (claim, defaultTitle, defaultDescription) {
|
||||
let openGraphData = {};
|
||||
openGraphData['embedUrl'] = `${site.host}/${claim.claimId}/${claim.name}`;
|
||||
openGraphData['showUrl'] = `${site.host}/${claim.claimId}/${claim.name}`;
|
||||
openGraphData['source'] = `${site.host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
|
||||
openGraphData['directFileUrl'] = `${site.host}/${claim.claimId}/${claim.name}.${claim.fileExt}`;
|
||||
openGraphData['ogTitle'] = determineOgTitle(claim.title, defaultTitle);
|
||||
openGraphData['ogDescription'] = determineOgDescription(claim.description, defaultDescription);
|
||||
openGraphData['ogThumbnailContentType'] = determineOgThumbnailContentType(claim.thumbnail);
|
||||
return openGraphData;
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
placeCommonHeaderTags () {
|
||||
const headerBoilerplate = `<meta charset="UTF-8"><meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1, user-scalable=no"><meta http-equiv="X-UA-Compatible" content="ie=edge"><title>${site.title}</title><link rel="stylesheet" href="/assets/css/reset.css" type="text/css"><link rel="stylesheet" href="/assets/css/general.css" type="text/css"><link rel="stylesheet" href="/assets/css/mediaQueries.css" type="text/css">`;
|
||||
return new Handlebars.SafeString(headerBoilerplate);
|
||||
},
|
||||
googleAnalytics () {
|
||||
const googleApiKey = analytics.googleId;
|
||||
const gaCode = `<script>(function(i,s,o,g,r,a,m){i['GoogleAnalyticsObject']=r;i[r]=i[r]||function(){
|
||||
(i[r].q=i[r].q||[]).push(arguments)},i[r].l=1*new Date();a=s.createElement(o),
|
||||
m=s.getElementsByTagName(o)[0];a.async=1;a.src=g;m.parentNode.insertBefore(a,m)
|
||||
})(window,document,'script','https://www.google-analytics.com/analytics.js','ga');
|
||||
ga('create', '${googleApiKey}', 'auto');
|
||||
ga('send', 'pageview');</script>`;
|
||||
return new Handlebars.SafeString(gaCode);
|
||||
},
|
||||
addOpenGraph (claim) {
|
||||
const { ogTitle, ogDescription, showUrl, source, ogThumbnailContentType } = createOpenGraphDataFromClaim(claim, claimDefaults.defaultTitle, claimDefaults.defaultDescription);
|
||||
const thumbnail = claim.thumbnail;
|
||||
const contentType = claim.contentType;
|
||||
const ogTitleTag = `<meta property="og:title" content="${ogTitle}" />`;
|
||||
const ogUrlTag = `<meta property="og:url" content="${showUrl}" />`;
|
||||
const ogSiteNameTag = `<meta property="og:site_name" content="${site.title}" />`;
|
||||
const ogDescriptionTag = `<meta property="og:description" content="${ogDescription}" />`;
|
||||
const ogImageWidthTag = '<meta property="og:image:width" content="600" />';
|
||||
const ogImageHeightTag = '<meta property="og:image:height" content="315" />';
|
||||
const basicTags = `${ogTitleTag} ${ogUrlTag} ${ogSiteNameTag} ${ogDescriptionTag} ${ogImageWidthTag} ${ogImageHeightTag}`;
|
||||
let ogImageTag = `<meta property="og:image" content="${source}" />`;
|
||||
let ogImageTypeTag = `<meta property="og:image:type" content="${contentType}" />`;
|
||||
let ogTypeTag = `<meta property="og:type" content="article" />`;
|
||||
if (contentType === 'video/mp4') {
|
||||
const ogVideoTag = `<meta property="og:video" content="${source}" />`;
|
||||
const ogVideoSecureUrlTag = `<meta property="og:video:secure_url" content="${source}" />`;
|
||||
const ogVideoTypeTag = `<meta property="og:video:type" content="${contentType}" />`;
|
||||
ogImageTag = `<meta property="og:image" content="${thumbnail}" />`;
|
||||
ogImageTypeTag = `<meta property="og:image:type" content="${ogThumbnailContentType}" />`;
|
||||
ogTypeTag = `<meta property="og:type" content="video" />`;
|
||||
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag} ${ogVideoTag} ${ogVideoSecureUrlTag} ${ogVideoTypeTag}`);
|
||||
} else {
|
||||
if (contentType === 'image/gif') {
|
||||
ogTypeTag = `<meta property="og:type" content="video.other" />`;
|
||||
};
|
||||
return new Handlebars.SafeString(`${basicTags} ${ogImageTag} ${ogImageTypeTag} ${ogTypeTag}`);
|
||||
}
|
||||
},
|
||||
addTwitterCard (claim) {
|
||||
const { embedUrl, directFileUrl } = createOpenGraphDataFromClaim(claim, claimDefaults.defaultTitle, claimDefaults.defaultDescription);
|
||||
const basicTwitterTags = `<meta name="twitter:site" content="@spee_ch" >`;
|
||||
const contentType = claim.contentType;
|
||||
if (contentType === 'video/mp4') {
|
||||
const twitterName = '<meta name="twitter:card" content="player" >';
|
||||
const twitterPlayer = `<meta name="twitter:player" content="${embedUrl}" >`;
|
||||
const twitterPlayerWidth = '<meta name="twitter:player:width" content="600" >';
|
||||
const twitterTextPlayerWidth = '<meta name="twitter:text:player_width" content="600" >';
|
||||
const twitterPlayerHeight = '<meta name="twitter:player:height" content="337" >';
|
||||
const twitterPlayerStream = `<meta name="twitter:player:stream" content="${directFileUrl}" >`;
|
||||
const twitterPlayerStreamContentType = '<meta name="twitter:player:stream:content_type" content="video/mp4" >';
|
||||
return new Handlebars.SafeString(`${basicTwitterTags} ${twitterName} ${twitterPlayer} ${twitterPlayerWidth} ${twitterTextPlayerWidth} ${twitterPlayerHeight} ${twitterPlayerStream} ${twitterPlayerStreamContentType}`);
|
||||
} else {
|
||||
const twitterCard = '<meta name="twitter:card" content="summary_large_image" >';
|
||||
return new Handlebars.SafeString(`${basicTwitterTags} ${twitterCard}`);
|
||||
}
|
||||
},
|
||||
ifConditional (varOne, operator, varTwo, options) {
|
||||
switch (operator) {
|
||||
case '===':
|
||||
return (varOne === varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case '!==':
|
||||
return (varOne !== varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case '<':
|
||||
return (varOne < varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case '<=':
|
||||
return (varOne <= varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case '>':
|
||||
return (varOne > varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case '>=':
|
||||
return (varOne >= varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case '&&':
|
||||
return (varOne && varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case '||':
|
||||
return (varOne || varTwo) ? options.fn(this) : options.inverse(this);
|
||||
case 'mod3':
|
||||
return ((parseInt(varOne) % 3) === 0) ? options.fn(this) : options.inverse(this);
|
||||
default:
|
||||
return options.inverse(this);
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,4 +1,3 @@
|
|||
const constants = require('../constants');
|
||||
const logger = require('winston');
|
||||
const fs = require('fs');
|
||||
const { site, wallet } = require('../config/speechConfig.js');
|
||||
|
@ -177,11 +176,4 @@ module.exports = {
|
|||
nsfw,
|
||||
};
|
||||
},
|
||||
returnPublishTimingActionType (channelName) {
|
||||
if (channelName) {
|
||||
return constants.PUBLISH_IN_CHANNEL_CLAIM;
|
||||
} else {
|
||||
return constants.PUBLISH_ANONYMOUS_CLAIM;
|
||||
}
|
||||
},
|
||||
};
|
||||
|
|
32
helpers/renderFullPage.js
Normal file
32
helpers/renderFullPage.js
Normal 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>
|
||||
`;
|
||||
};
|
|
@ -1,25 +1,109 @@
|
|||
const logger = require('winston');
|
||||
const { getClaimId, getLocalFileRecord } = require('../controllers/serveController.js');
|
||||
const { handleErrorResponse } = require('../helpers/errorHandlers.js');
|
||||
|
||||
const SERVE = 'SERVE';
|
||||
const SHOW = 'SHOW';
|
||||
const NO_FILE = 'NO_FILE';
|
||||
const NO_CHANNEL = 'NO_CHANNEL';
|
||||
const NO_CLAIM = 'NO_CLAIM';
|
||||
|
||||
function clientAcceptsHtml ({accept}) {
|
||||
return accept && accept.match(/text\/html/);
|
||||
};
|
||||
|
||||
function requestIsFromBrowser (headers) {
|
||||
return headers['user-agent'] && headers['user-agent'].match(/Mozilla/);
|
||||
};
|
||||
|
||||
function clientWantsAsset ({accept, range}) {
|
||||
const imageIsWanted = accept && accept.match(/image\/.*/) && !accept.match(/text\/html/) && !accept.match(/text\/\*/);
|
||||
const videoIsWanted = accept && range;
|
||||
return imageIsWanted || videoIsWanted;
|
||||
};
|
||||
|
||||
function isValidClaimId (claimId) {
|
||||
return ((claimId.length === 40) && !/[^A-Za-z0-9]/g.test(claimId));
|
||||
};
|
||||
|
||||
function isValidShortId (claimId) {
|
||||
return claimId.length === 1; // it should really evaluate the short url itself
|
||||
};
|
||||
|
||||
function isValidShortIdOrClaimId (input) {
|
||||
return (isValidClaimId(input) || isValidShortId(input));
|
||||
};
|
||||
|
||||
function serveAssetToClient (claimId, name, res) {
|
||||
return getLocalFileRecord(claimId, name)
|
||||
.then(fileRecord => {
|
||||
// check that a local record was found
|
||||
if (fileRecord === NO_FILE) {
|
||||
return res.status(307).redirect(`/api/claim/get/${name}/${claimId}`);
|
||||
}
|
||||
// serve the file
|
||||
const {filePath, fileType} = fileRecord;
|
||||
logger.verbose(`serving file: ${filePath}`);
|
||||
const sendFileOptions = {
|
||||
headers: {
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Content-Type' : fileType || 'image/jpeg',
|
||||
},
|
||||
};
|
||||
res.status(200).sendFile(filePath, sendFileOptions);
|
||||
})
|
||||
.catch(error => {
|
||||
throw error;
|
||||
});
|
||||
};
|
||||
|
||||
module.exports = {
|
||||
serveFile ({ filePath, fileType }, claimId, name, res) {
|
||||
logger.verbose(`serving file: ${filePath}`);
|
||||
// set response options
|
||||
const headerContentType = fileType || 'image/jpeg';
|
||||
const options = {
|
||||
headers: {
|
||||
'X-Content-Type-Options': 'nosniff',
|
||||
'Content-Type' : headerContentType,
|
||||
},
|
||||
};
|
||||
// send the file
|
||||
res.status(200).sendFile(filePath, options);
|
||||
getClaimIdAndServeAsset (channelName, channelClaimId, claimName, claimId, originalUrl, ip, res) {
|
||||
// get the claim Id and then serve the asset
|
||||
getClaimId(channelName, channelClaimId, claimName, claimId)
|
||||
.then(fullClaimId => {
|
||||
if (fullClaimId === NO_CLAIM) {
|
||||
return res.status(404).json({success: false, message: 'no claim id could be found'});
|
||||
} else if (fullClaimId === NO_CHANNEL) {
|
||||
return res.status(404).json({success: false, message: 'no channel id could be found'});
|
||||
}
|
||||
serveAssetToClient(fullClaimId, claimName, res);
|
||||
// postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'success');
|
||||
})
|
||||
.catch(error => {
|
||||
handleErrorResponse(originalUrl, ip, error, res);
|
||||
// postToStats(responseType, originalUrl, ip, claimName, fullClaimId, 'fail');
|
||||
});
|
||||
},
|
||||
showFile (claimInfo, shortId, res) {
|
||||
logger.verbose(`showing claim: ${claimInfo.name}#${claimInfo.claimId}`);
|
||||
res.status(200).render('index');
|
||||
determineResponseType (hasFileExtension, headers) {
|
||||
let responseType;
|
||||
if (hasFileExtension) {
|
||||
responseType = SERVE; // assume a serve request if file extension is present
|
||||
if (clientAcceptsHtml(headers)) { // if the request comes from a browser, change it to a show request
|
||||
responseType = SHOW;
|
||||
}
|
||||
} else {
|
||||
responseType = SHOW;
|
||||
if (clientWantsAsset(headers) && requestIsFromBrowser(headers)) { // this is in case someone embeds a show url
|
||||
logger.debug('Show request came from browser but wants an image/video. Changing response to serve...');
|
||||
responseType = SERVE;
|
||||
}
|
||||
}
|
||||
return responseType;
|
||||
},
|
||||
showFileLite (claimInfo, shortId, res) {
|
||||
logger.verbose(`showlite claim: ${claimInfo.name}#${claimInfo.claimId}`);
|
||||
res.status(200).render('index');
|
||||
flipClaimNameAndIdForBackwardsCompatibility (identifier, name) {
|
||||
// this is a patch for backwards compatability with '/name/claim_id' url format
|
||||
if (isValidShortIdOrClaimId(name) && !isValidShortIdOrClaimId(identifier)) {
|
||||
const tempName = name;
|
||||
name = identifier;
|
||||
identifier = tempName;
|
||||
}
|
||||
return [identifier, name];
|
||||
},
|
||||
logRequestData (responseType, claimName, channelName, claimId) {
|
||||
logger.debug('responseType ===', responseType);
|
||||
logger.debug('claim name === ', claimName);
|
||||
logger.debug('channel name ===', channelName);
|
||||
logger.debug('claim id ===', claimId);
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,22 +1,7 @@
|
|||
const constants = require('../constants');
|
||||
const logger = require('winston');
|
||||
const ua = require('universal-analytics');
|
||||
const config = require('../config/speechConfig.js');
|
||||
const googleApiKey = config.analytics.googleId;
|
||||
const db = require('../models');
|
||||
|
||||
module.exports = {
|
||||
createPublishTimingEventParams (publishDurration, ip, headers, label) {
|
||||
return {
|
||||
userTimingCategory : 'lbrynet',
|
||||
userTimingVariableName: 'publish',
|
||||
userTimingTime : publishDurration,
|
||||
userTimingLabel : label,
|
||||
uip : ip,
|
||||
ua : headers['user-agent'],
|
||||
ul : headers['accept-language'],
|
||||
};
|
||||
},
|
||||
postToStats (action, url, ipAddress, name, claimId, result) {
|
||||
logger.debug('action:', action);
|
||||
// make sure the result is a string
|
||||
|
@ -50,46 +35,4 @@ module.exports = {
|
|||
logger.error('Sequelize error >>', error);
|
||||
});
|
||||
},
|
||||
sendGoogleAnalyticsEvent (action, headers, ip, originalUrl) {
|
||||
const visitorId = ip.replace(/\./g, '-');
|
||||
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
|
||||
let params;
|
||||
switch (action) {
|
||||
case 'SERVE':
|
||||
params = {
|
||||
ec : 'serve',
|
||||
ea : originalUrl,
|
||||
uip: ip,
|
||||
ua : headers['user-agent'],
|
||||
ul : headers['accept-language'],
|
||||
};
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
visitor.event(params, (err) => {
|
||||
if (err) {
|
||||
logger.error('Google Analytics Event Error >>', err);
|
||||
}
|
||||
});
|
||||
},
|
||||
sendGoogleAnalyticsTiming (action, headers, ip, originalUrl, startTime, endTime) {
|
||||
const visitorId = ip.replace(/\./g, '-');
|
||||
const visitor = ua(googleApiKey, visitorId, { strictCidFormat: false, https: true });
|
||||
const durration = endTime - startTime;
|
||||
let params;
|
||||
switch (action) {
|
||||
case constants.PUBLISH_ANONYMOUS_CLAIM:
|
||||
case constants.PUBLISH_IN_CHANNEL_CLAIM:
|
||||
logger.verbose(`${action} completed successfully in ${durration}ms`);
|
||||
params = module.exports.createPublishTimingEventParams(durration, ip, headers, action);
|
||||
break;
|
||||
default: break;
|
||||
}
|
||||
visitor.timing(params, (err) => {
|
||||
if (err) {
|
||||
logger.error('Google Analytics Event Error >>', err);
|
||||
}
|
||||
logger.debug(`${action} timing event successfully sent to google analytics`);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -3,11 +3,9 @@ const express = require('express');
|
|||
const bodyParser = require('body-parser');
|
||||
const expressHandlebars = require('express-handlebars');
|
||||
const Handlebars = require('handlebars');
|
||||
const handlebarsHelpers = require('./helpers/handlebarsHelpers.js');
|
||||
const { populateLocalsDotUser, serializeSpeechUser, deserializeSpeechUser } = require('./helpers/authHelpers.js');
|
||||
const config = require('./config/speechConfig.js');
|
||||
const logger = require('winston');
|
||||
const { getDownloadDirectory } = require('./helpers/lbryApi');
|
||||
const helmet = require('helmet');
|
||||
const PORT = 3000; // set port
|
||||
const app = express(); // create an Express application
|
||||
|
@ -54,9 +52,8 @@ app.use(passport.session());
|
|||
|
||||
// configure handlebars & register it with express app
|
||||
const hbs = expressHandlebars.create({
|
||||
defaultLayout: 'main', // sets the default layout
|
||||
defaultLayout: 'embed', // sets the default layout
|
||||
handlebars : Handlebars, // includes basic handlebars for access to that library
|
||||
helpers : handlebarsHelpers, // custom defined helpers
|
||||
});
|
||||
app.engine('handlebars', hbs.engine);
|
||||
app.set('view engine', 'handlebars');
|
||||
|
@ -67,19 +64,12 @@ app.use(populateLocalsDotUser);
|
|||
// start the server
|
||||
db.sequelize
|
||||
.sync() // sync sequelize
|
||||
.then(() => { // get the download directory from the daemon
|
||||
logger.info('Retrieving daemon download directory...');
|
||||
return getDownloadDirectory();
|
||||
})
|
||||
.then(hostedContentPath => {
|
||||
// add the hosted content folder at a static path
|
||||
app.use('/media', express.static(hostedContentPath));
|
||||
// require routes
|
||||
.then(() => { // require routes
|
||||
require('./routes/auth-routes.js')(app);
|
||||
require('./routes/api-routes.js')(app);
|
||||
require('./routes/page-routes.js')(app);
|
||||
require('./routes/serve-routes.js')(app);
|
||||
require('./routes/home-routes.js')(app);
|
||||
require('./routes/fallback-routes.js')(app);
|
||||
const http = require('http');
|
||||
return http.Server(app);
|
||||
})
|
|
@ -176,15 +176,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
this.findOne({
|
||||
where: {name, claimId},
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -318,15 +318,15 @@ module.exports = (sequelize, { STRING, BOOLEAN, INTEGER, TEXT, DECIMAL }) => {
|
|||
this.findOne({
|
||||
where: {name, claimId},
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
return resolve(null);
|
||||
};
|
||||
resolve(claimId);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
// const fs = require('fs');
|
||||
// const path = require('path');
|
||||
const Sequelize = require('sequelize');
|
||||
const basename = path.basename(module.filename);
|
||||
// const basename = path.basename(module.filename);
|
||||
const logger = require('winston');
|
||||
const config = require('../config/speechConfig.js');
|
||||
const { database, username, password } = config.sql;
|
||||
|
@ -30,16 +30,19 @@ sequelize
|
|||
logger.error('Sequelize was unable to connect to the database:', err);
|
||||
});
|
||||
|
||||
// add each model to the db object
|
||||
fs
|
||||
.readdirSync(__dirname)
|
||||
.filter(file => {
|
||||
return (file.indexOf('.') !== 0 && file !== basename && file.slice(-3) === '.js');
|
||||
})
|
||||
.forEach(file => {
|
||||
const model = sequelize['import'](path.join(__dirname, file));
|
||||
db[model.name] = model;
|
||||
});
|
||||
// manually add each model to the db
|
||||
const Certificate = require('./certificate.js');
|
||||
const Channel = require('./channel.js');
|
||||
const Claim = require('./claim.js');
|
||||
const File = require('./file.js');
|
||||
const Request = require('./request.js');
|
||||
const User = require('./user.js');
|
||||
db['Certificate'] = sequelize.import('Certificate', Certificate);
|
||||
db['Channel'] = sequelize.import('Channel', Channel);
|
||||
db['Claim'] = sequelize.import('Claim', Claim);
|
||||
db['File'] = sequelize.import('File', File);
|
||||
db['Request'] = sequelize.import('Request', Request);
|
||||
db['User'] = sequelize.import('User', User);
|
||||
|
||||
// run model.association for each model in the db object that has an association
|
||||
Object.keys(db).forEach(modelName => {
|
||||
|
|
37
package.json
37
package.json
|
@ -6,12 +6,14 @@
|
|||
"scripts": {
|
||||
"test": "mocha --recursive",
|
||||
"test-all": "mocha --recursive",
|
||||
"start": "node speech.js",
|
||||
"start": "node server.js",
|
||||
"start-dev": "nodemon server.js",
|
||||
"lint": "eslint .",
|
||||
"fix": "eslint . --fix",
|
||||
"precommit": "eslint .",
|
||||
"babel": "babel",
|
||||
"webpack": "webpack"
|
||||
"build-dev": "webpack --config webpack.dev.js",
|
||||
"build-prod": "webpack --config webpack.prod.js"
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
@ -35,26 +37,31 @@
|
|||
"config": "^1.26.1",
|
||||
"connect-multiparty": "^2.0.0",
|
||||
"cookie-session": "^2.0.0-beta.3",
|
||||
"cross-fetch": "^1.1.1",
|
||||
"express": "^4.15.2",
|
||||
"express-handlebars": "^3.0.0",
|
||||
"form-data": "^2.3.1",
|
||||
"helmet": "^3.8.1",
|
||||
"mysql2": "^1.3.5",
|
||||
"nodemon": "^1.11.0",
|
||||
"node-fetch": "^2.0.0",
|
||||
"passport": "^0.4.0",
|
||||
"passport-local": "^1.0.0",
|
||||
"prop-types": "^15.6.0",
|
||||
"react": "^16.2.0",
|
||||
"react-dom": "^16.2.0",
|
||||
"react-ga": "^2.4.1",
|
||||
"react-helmet": "^5.2.0",
|
||||
"react-redux": "^5.0.6",
|
||||
"react-router-dom": "^4.2.2",
|
||||
"redux": "^3.7.2",
|
||||
"redux-saga": "^0.16.0",
|
||||
"request": "^2.83.0",
|
||||
"request-promise": "^4.2.2",
|
||||
"sequelize": "^4.1.0",
|
||||
"sequelize-cli": "^3.0.0-3",
|
||||
"sleep": "^5.1.1",
|
||||
"universal-analytics": "^0.4.13",
|
||||
"webpack-node-externals": "^1.6.0",
|
||||
"whatwg-fetch": "^2.0.3",
|
||||
"winston": "^2.3.1",
|
||||
"winston-slack-webhook": "billbitt/winston-slack-webhook"
|
||||
|
@ -62,21 +69,29 @@
|
|||
"devDependencies": {
|
||||
"babel-core": "^6.26.0",
|
||||
"babel-loader": "^7.1.2",
|
||||
"babel-plugin-transform-object-rest-spread": "^6.26.0",
|
||||
"babel-polyfill": "^6.26.0",
|
||||
"babel-preset-es2015": "^6.24.1",
|
||||
"babel-preset-react": "^6.24.1",
|
||||
"babel-preset-stage-2": "^6.24.1",
|
||||
"babel-register": "^6.26.0",
|
||||
"chai": "^4.1.2",
|
||||
"chai-http": "^3.0.0",
|
||||
"eslint": "3.19.0",
|
||||
"eslint-config-standard": "10.2.1",
|
||||
"eslint-plugin-import": "^2.2.0",
|
||||
"eslint-plugin-node": "^4.2.2",
|
||||
"eslint-plugin-promise": "3.5.0",
|
||||
"eslint-plugin-react": "6.10.3",
|
||||
"eslint-plugin-standard": "3.0.1",
|
||||
"css-loader": "^0.28.9",
|
||||
"eslint": "4.18.0",
|
||||
"eslint-config-standard": "^10.2.1",
|
||||
"eslint-config-standard-jsx": "^5.0.0",
|
||||
"eslint-plugin-import": "^2.8.0",
|
||||
"eslint-plugin-node": "^4.2.3",
|
||||
"eslint-plugin-promise": "^3.5.0",
|
||||
"eslint-plugin-react": "^7.6.1",
|
||||
"eslint-plugin-standard": "^3.0.1",
|
||||
"husky": "^0.13.4",
|
||||
"mocha": "^4.0.1",
|
||||
"nodemon": "^1.15.1",
|
||||
"redux-devtools": "^3.4.1",
|
||||
"webpack": "^3.10.0"
|
||||
"regenerator-transform": "^0.12.3",
|
||||
"webpack": "^3.10.0",
|
||||
"webpack-merge": "^4.1.2"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,19 +8,19 @@ function returnUserAndChannelInfo (userInstance) {
|
|||
userInfo['id'] = userInstance.id;
|
||||
userInfo['userName'] = userInstance.userName;
|
||||
userInstance
|
||||
.getChannel()
|
||||
.then(({channelName, channelClaimId}) => {
|
||||
userInfo['channelName'] = channelName;
|
||||
userInfo['channelClaimId'] = channelClaimId;
|
||||
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
|
||||
})
|
||||
.then(shortChannelId => {
|
||||
userInfo['shortChannelId'] = shortChannelId;
|
||||
resolve(userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
.getChannel()
|
||||
.then(({channelName, channelClaimId}) => {
|
||||
userInfo['channelName'] = channelName;
|
||||
userInfo['channelClaimId'] = channelClaimId;
|
||||
return db.Certificate.getShortChannelIdFromLongChannelId(channelClaimId, channelName);
|
||||
})
|
||||
.then(shortChannelId => {
|
||||
userInfo['shortChannelId'] = shortChannelId;
|
||||
resolve(userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -32,34 +32,34 @@ module.exports = new PassportLocalStrategy(
|
|||
(username, password, done) => {
|
||||
logger.debug('logging user in');
|
||||
return db
|
||||
.User
|
||||
.findOne({where: {userName: username}})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
// logger.debug('no user found');
|
||||
.User
|
||||
.findOne({where: {userName: username}})
|
||||
.then(user => {
|
||||
if (!user) {
|
||||
// logger.debug('no user found');
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
user.comparePassword(password, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('passwordErr:', passwordErr);
|
||||
return done(null, false, {message: passwordErr});
|
||||
}
|
||||
if (!isMatch) {
|
||||
// logger.debug('incorrect password');
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
user.comparePassword(password, (passwordErr, isMatch) => {
|
||||
if (passwordErr) {
|
||||
logger.error('passwordErr:', passwordErr);
|
||||
return done(null, false, {message: passwordErr});
|
||||
}
|
||||
if (!isMatch) {
|
||||
// logger.debug('incorrect password');
|
||||
return done(null, false, {message: 'Incorrect username or password'});
|
||||
}
|
||||
logger.debug('Password was a match, returning User');
|
||||
return returnUserAndChannelInfo(user)
|
||||
.then((userInfo) => {
|
||||
return done(null, userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
logger.debug('Password was a match, returning User');
|
||||
return returnUserAndChannelInfo(user)
|
||||
.then((userInfo) => {
|
||||
return done(null, userInfo);
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
});
|
||||
});
|
||||
})
|
||||
.catch(error => {
|
||||
return done(error);
|
||||
});
|
||||
}
|
||||
);
|
||||
|
|
|
@ -276,7 +276,7 @@ a, a:visited {
|
|||
vertical-align: top;
|
||||
}
|
||||
|
||||
.align-content-right {
|
||||
.align-content-bottom {
|
||||
vertical-align: bottom;
|
||||
}
|
||||
|
||||
|
@ -407,12 +407,13 @@ button {
|
|||
cursor: pointer;
|
||||
}
|
||||
|
||||
.button--primary {
|
||||
.button--primary, .button--primary:focus {
|
||||
border: 1px solid black;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0.3em 0.5em 0.3em;
|
||||
color: black;
|
||||
background-color: white;
|
||||
outline: 0px;
|
||||
}
|
||||
|
||||
.button--primary:hover {
|
||||
|
@ -422,9 +423,28 @@ button {
|
|||
}
|
||||
|
||||
.button--primary:active{
|
||||
border: 1px solid #4156C5;
|
||||
color: white;
|
||||
border: 1px solid #ffffff;
|
||||
color: #d0d0d0;
|
||||
background-color: #ffffff;
|
||||
}
|
||||
|
||||
.button--secondary, .button--secondary:focus {
|
||||
border: 0px;
|
||||
border-bottom: 1px solid black;
|
||||
padding: 0.5em;
|
||||
margin: 0.5em 0.3em 0.5em 0.3em;
|
||||
color: black;
|
||||
background-color: white;
|
||||
outline: 0px;
|
||||
}
|
||||
|
||||
.button--secondary:hover {
|
||||
border-bottom: 1px solid #9b9b9b;
|
||||
color: #4156C5;
|
||||
}
|
||||
|
||||
.button--secondary:active {
|
||||
color: #ffffff;;
|
||||
}
|
||||
|
||||
.button--large{
|
||||
|
@ -495,7 +515,7 @@ table {
|
|||
padding: 1em;
|
||||
}
|
||||
|
||||
#asset-preview {
|
||||
#dropzone-preview {
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
@ -506,26 +526,24 @@ table {
|
|||
|
||||
/* Assets */
|
||||
|
||||
.asset {
|
||||
width: 100%;
|
||||
.asset-holder {
|
||||
clear : both;
|
||||
display: inline-block;
|
||||
width : 31%;
|
||||
padding: 0px;
|
||||
margin : 1%;
|
||||
}
|
||||
|
||||
#show-body #asset-boilerpate {
|
||||
display: none;
|
||||
.asset-preview {
|
||||
width : 100%;
|
||||
padding: 0px;
|
||||
margin : 0px
|
||||
}
|
||||
|
||||
#showlite-body #asset-display-component {
|
||||
max-width: 50%;
|
||||
text-align: center;
|
||||
}
|
||||
/* show */
|
||||
|
||||
/* video */
|
||||
|
||||
#video-asset {
|
||||
background-color: #000000;
|
||||
#video {
|
||||
cursor: pointer;
|
||||
}
|
||||
#showlite-body #video-asset {
|
||||
background-color: #ffffff;
|
||||
width: calc(100% - 12px - 12px - 2px);
|
||||
margin: 6px;
|
||||
|
@ -533,6 +551,29 @@ table {
|
|||
border: 1px solid #d0d0d0;
|
||||
}
|
||||
|
||||
/* show lite */
|
||||
.show-lite-container {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.show-lite-container #asset-display-component {
|
||||
max-height: calc(100vh - 3em);
|
||||
}
|
||||
|
||||
.show-details-container #asset-display-component .asset {
|
||||
width: 100%
|
||||
}
|
||||
|
||||
.show-lite-container #asset-display-component .asset {
|
||||
max-height: calc(100vh - 3em);
|
||||
max-width: 100vw;
|
||||
}
|
||||
|
||||
#asset-boilerplate {
|
||||
max-height: 3em;
|
||||
}
|
||||
|
||||
|
||||
/* item lists */
|
||||
|
||||
.content-list-item-asset {
|
||||
|
|
25176
public/bundle/bundle.js
25176
public/bundle/bundle.js
File diff suppressed because it is too large
Load diff
|
@ -5,8 +5,10 @@ import * as actions from 'constants/channel_action_types';
|
|||
export function updateLoggedInChannel (name, shortId, longId) {
|
||||
return {
|
||||
type: actions.CHANNEL_UPDATE,
|
||||
name,
|
||||
shortId,
|
||||
longId,
|
||||
data: {
|
||||
name,
|
||||
shortId,
|
||||
longId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
|
|
@ -4,7 +4,7 @@ import * as actions from 'constants/publish_action_types';
|
|||
export function selectFile (file) {
|
||||
return {
|
||||
type: actions.FILE_SELECTED,
|
||||
file: file,
|
||||
data: file,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -17,15 +17,17 @@ export function clearFile () {
|
|||
export function updateMetadata (name, value) {
|
||||
return {
|
||||
type: actions.METADATA_UPDATE,
|
||||
name,
|
||||
value,
|
||||
data: {
|
||||
name,
|
||||
value,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function updateClaim (value) {
|
||||
return {
|
||||
type: actions.CLAIM_UPDATE,
|
||||
value,
|
||||
data: value,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -39,30 +41,34 @@ export function setPublishInChannel (channel) {
|
|||
export function updatePublishStatus (status, message) {
|
||||
return {
|
||||
type: actions.PUBLISH_STATUS_UPDATE,
|
||||
status,
|
||||
message,
|
||||
data: {
|
||||
status,
|
||||
message,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function updateError (name, value) {
|
||||
return {
|
||||
type: actions.ERROR_UPDATE,
|
||||
name,
|
||||
value,
|
||||
data: {
|
||||
name,
|
||||
value,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function updateSelectedChannel (value) {
|
||||
export function updateSelectedChannel (channelName) {
|
||||
return {
|
||||
type: actions.SELECTED_CHANNEL_UPDATE,
|
||||
value,
|
||||
data: channelName,
|
||||
};
|
||||
};
|
||||
|
||||
export function toggleMetadataInputs (value) {
|
||||
export function toggleMetadataInputs (showMetadataInputs) {
|
||||
return {
|
||||
type: actions.TOGGLE_METADATA_INPUTS,
|
||||
value,
|
||||
data: showMetadataInputs,
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -1,48 +1,119 @@
|
|||
import * as actions from 'constants/show_action_types';
|
||||
// export action creators
|
||||
|
||||
export function updateRequestWithChannelRequest (name, id) {
|
||||
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
|
||||
|
||||
// basic request parsing
|
||||
export function onHandleShowPageUri (params) {
|
||||
return {
|
||||
type: actions.REQUEST_UPDATE_CHANNEL,
|
||||
name,
|
||||
id,
|
||||
type: actions.HANDLE_SHOW_URI,
|
||||
data: params,
|
||||
};
|
||||
};
|
||||
|
||||
export function updateRequestWithAssetRequest (name, id, channelName, channelId, extension) {
|
||||
export function onRequestError (error) {
|
||||
return {
|
||||
type : actions.REQUEST_UPDATE_CLAIM,
|
||||
name,
|
||||
id,
|
||||
channelName: null,
|
||||
channelId : null,
|
||||
extension,
|
||||
type: actions.REQUEST_ERROR,
|
||||
data: error,
|
||||
};
|
||||
};
|
||||
|
||||
export function updateChannelData (name, longId, shortId) {
|
||||
export function onNewChannelRequest (channelName, channelId) {
|
||||
const requestType = CHANNEL;
|
||||
const requestId = `cr#${channelName}#${channelId}`;
|
||||
return {
|
||||
type: actions.CHANNEL_DATA_UPDATE,
|
||||
name,
|
||||
longId,
|
||||
shortId,
|
||||
type: actions.CHANNEL_REQUEST_NEW,
|
||||
data: { requestType, requestId, channelName, channelId },
|
||||
};
|
||||
};
|
||||
|
||||
export function updateChannelClaimsData (claims, currentPage, totalPages, totalClaims) {
|
||||
export function onNewAssetRequest (name, id, channelName, channelId, extension) {
|
||||
const requestType = extension ? ASSET_LITE : ASSET_DETAILS;
|
||||
const requestId = `ar#${name}#${id}#${channelName}#${channelId}`;
|
||||
return {
|
||||
type: actions.CHANNEL_CLAIMS_DATA_UPDATE,
|
||||
claims,
|
||||
currentPage,
|
||||
totalPages,
|
||||
totalClaims,
|
||||
type: actions.ASSET_REQUEST_NEW,
|
||||
data: {
|
||||
requestType,
|
||||
requestId,
|
||||
name,
|
||||
modifier: {
|
||||
id,
|
||||
channel: {
|
||||
name: channelName,
|
||||
id : channelId,
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function updateAssetClaimData (data, shortId) {
|
||||
export function onRequestUpdate (requestType, requestId) {
|
||||
return {
|
||||
type: actions.ASSET_CLAIM_DATA_UPDATE,
|
||||
data,
|
||||
shortId,
|
||||
type: actions.REQUEST_UPDATE,
|
||||
data: {
|
||||
requestType,
|
||||
requestId,
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export function addRequestToRequestList (id, error, key) {
|
||||
return {
|
||||
type: actions.REQUEST_LIST_ADD,
|
||||
data: { id, error, key },
|
||||
};
|
||||
};
|
||||
|
||||
// asset actions
|
||||
|
||||
export function addAssetToAssetList (id, error, name, claimId, shortId, claimData) {
|
||||
return {
|
||||
type: actions.ASSET_ADD,
|
||||
data: { id, error, name, claimId, shortId, claimData },
|
||||
};
|
||||
}
|
||||
|
||||
// channel actions
|
||||
|
||||
export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) {
|
||||
return {
|
||||
type: actions.CHANNEL_ADD,
|
||||
data: { id, name, shortId, longId, claimsData },
|
||||
};
|
||||
};
|
||||
|
||||
export function onUpdateChannelClaims (channelKey, name, longId, page) {
|
||||
return {
|
||||
type: actions.CHANNEL_CLAIMS_UPDATE_ASYNC,
|
||||
data: {channelKey, name, longId, page},
|
||||
};
|
||||
};
|
||||
|
||||
export function updateChannelClaims (channelListId, claimsData) {
|
||||
return {
|
||||
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCESS,
|
||||
data: {channelListId, claimsData},
|
||||
};
|
||||
};
|
||||
|
||||
// display a file
|
||||
|
||||
export function fileRequested (name, claimId) {
|
||||
return {
|
||||
type: actions.FILE_REQUESTED,
|
||||
data: { name, claimId },
|
||||
};
|
||||
};
|
||||
|
||||
export function updateFileAvailability (status) {
|
||||
return {
|
||||
type: actions.FILE_AVAILABILITY_UPDATE,
|
||||
data: status,
|
||||
};
|
||||
};
|
||||
|
||||
export function updateDisplayAssetError (error) {
|
||||
return {
|
||||
type: actions.DISPLAY_ASSET_ERROR,
|
||||
data: error,
|
||||
};
|
||||
};
|
||||
|
|
38
react/api/assetApi.js
Normal file
38
react/api/assetApi.js
Normal file
|
@ -0,0 +1,38 @@
|
|||
import Request from 'utils/request';
|
||||
const { site: { host } } = require('../../config/speechConfig.js');
|
||||
|
||||
export function getLongClaimId (name, modifier) {
|
||||
// console.log('getting long claim id for asset:', name, modifier);
|
||||
let body = {};
|
||||
// create request params
|
||||
if (modifier) {
|
||||
if (modifier.id) {
|
||||
body['claimId'] = modifier.id;
|
||||
} else {
|
||||
body['channelName'] = modifier.channel.name;
|
||||
body['channelClaimId'] = modifier.channel.id;
|
||||
}
|
||||
}
|
||||
body['claimName'] = name;
|
||||
const params = {
|
||||
method : 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body : JSON.stringify(body),
|
||||
};
|
||||
// create url
|
||||
const url = `${host}/api/claim/long-id`;
|
||||
// return the request promise
|
||||
return Request(url, params);
|
||||
};
|
||||
|
||||
export function getShortId (name, claimId) {
|
||||
// console.log('getting short id for asset:', name, claimId);
|
||||
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
|
||||
return Request(url);
|
||||
};
|
||||
|
||||
export function getClaimData (name, claimId) {
|
||||
// console.log('getting claim data for asset:', name, claimId);
|
||||
const url = `${host}/api/claim/data/${name}/${claimId}`;
|
||||
return Request(url);
|
||||
};
|
16
react/api/channelApi.js
Normal file
16
react/api/channelApi.js
Normal file
|
@ -0,0 +1,16 @@
|
|||
import Request from 'utils/request';
|
||||
const { site: { host } } = require('../../config/speechConfig.js');
|
||||
|
||||
export function getChannelData (name, id) {
|
||||
console.log('getting channel data for channel:', name, id);
|
||||
if (!id) id = 'none';
|
||||
const url = `${host}/api/channel/data/${name}/${id}`;
|
||||
return Request(url);
|
||||
};
|
||||
|
||||
export function getChannelClaims (name, longId, page) {
|
||||
console.log('getting channel claims for channel:', name, longId);
|
||||
if (!page) page = 1;
|
||||
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
|
||||
return Request(url);
|
||||
};
|
12
react/api/fileApi.js
Normal file
12
react/api/fileApi.js
Normal file
|
@ -0,0 +1,12 @@
|
|||
import Request from 'utils/request';
|
||||
const { site: { host } } = require('../../config/speechConfig.js');
|
||||
|
||||
export function checkFileAvailability (name, claimId) {
|
||||
const url = `${host}/api/file/availability/${name}/${claimId}`;
|
||||
return Request(url);
|
||||
}
|
||||
|
||||
export function triggerClaimGet (name, claimId) {
|
||||
const url = `${host}/api/claim/get/${name}/${claimId}`;
|
||||
return Request(url);
|
||||
}
|
22
react/app.js
Normal file
22
react/app.js
Normal 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
45
react/client.js
Normal file
|
@ -0,0 +1,45 @@
|
|||
import React from 'react';
|
||||
import { hydrate } from 'react-dom';
|
||||
import { Provider } from 'react-redux';
|
||||
import { createStore, applyMiddleware, compose } from 'redux';
|
||||
import { BrowserRouter } from 'react-router-dom';
|
||||
import Reducer from 'reducers';
|
||||
import createSagaMiddleware from 'redux-saga';
|
||||
import rootSaga from 'sagas';
|
||||
|
||||
import GAListener from 'components/GAListener';
|
||||
import App from './app';
|
||||
|
||||
// get the state from a global variable injected into the server-generated HTML
|
||||
const preloadedState = window.__PRELOADED_STATE__ || null;
|
||||
|
||||
// Allow the passed state to be garbage-collected
|
||||
delete window.__PRELOADED_STATE__;
|
||||
|
||||
// create and apply middleware
|
||||
const sagaMiddleware = createSagaMiddleware();
|
||||
const middleware = applyMiddleware(sagaMiddleware);
|
||||
const reduxMiddleware = window.__REDUX_DEVTOOLS_EXTENSION__ ? compose(middleware, window.__REDUX_DEVTOOLS_EXTENSION__()) : middleware;
|
||||
|
||||
// create teh store
|
||||
let store;
|
||||
if (preloadedState) {
|
||||
store = createStore(Reducer, preloadedState, reduxMiddleware);
|
||||
} else {
|
||||
store = createStore(Reducer, reduxMiddleware);
|
||||
}
|
||||
|
||||
// run the saga middlweare
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
// render the app
|
||||
hydrate(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<GAListener>
|
||||
<App />
|
||||
</GAListener>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
document.getElementById('react-app')
|
||||
);
|
|
@ -1,27 +1,29 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import SEO from 'components/SEO';
|
||||
|
||||
class AboutPage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<p className="pull-quote">Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
|
||||
<p><a className="link--primary" target="_blank" href="https://twitter.com/spee_ch">TWITTER</a></p>
|
||||
<p><a className="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch">GITHUB</a></p>
|
||||
<p><a className="link--primary" target="_blank" href="https://discord.gg/YjYbwhS">DISCORD CHANNEL</a></p>
|
||||
<p><a className="link--primary" target="_blank" href="https://github.com/lbryio/spee.ch/blob/master/README.md">DOCUMENTATION</a></p>
|
||||
<SEO pageTitle={'About'} pageUri={'about'} />
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<div className='column column--5 column--med-10 align-content-top'>
|
||||
<div className='column column--8 column--med-10'>
|
||||
<p className='pull-quote'>Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
|
||||
<p><a className='link--primary' target='_blank' href='https://twitter.com/spee_ch'>TWITTER</a></p>
|
||||
<p><a className='link--primary' target='_blank' href='https://github.com/lbryio/spee.ch'>GITHUB</a></p>
|
||||
<p><a className='link--primary' target='_blank' href='https://discord.gg/YjYbwhS'>DISCORD CHANNEL</a></p>
|
||||
<p><a className='link--primary' target='_blank' href='https://github.com/lbryio/spee.ch/blob/master/README.md'>DOCUMENTATION</a></p>
|
||||
</div>
|
||||
</div><div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a className="link--primary" href="https://lbry.io">LBRY</a> blockchain.</p>
|
||||
</div><div className='column column--5 column--med-10 align-content-top'>
|
||||
<div className='column column--8 column--med-10'>
|
||||
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a className='link--primary' href='https://lbry.io'>LBRY</a> blockchain.</p>
|
||||
<p>Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the LBRY network. This means that your images are stored in multiple locations without a single point of failure.</p>
|
||||
<h3>Contribute</h3>
|
||||
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a className="link--primary" href="https://github.com/lbryio/spee.ch">github repo</a> and go to town!</p>
|
||||
<p>If you want to improve spee.ch, join our <a className="link--primary" href="https://discord.gg/YjYbwhS">discord channel</a> or solve one of our <a className="link--primary" href="https://github.com/lbryio/spee.ch/issues">github issues</a>.</p>
|
||||
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a className='link--primary' href='https://github.com/lbryio/spee.ch'>github repo</a> and go to town!</p>
|
||||
<p>If you want to improve spee.ch, join our <a className='link--primary' href='https://discord.gg/YjYbwhS'>discord channel</a> or solve one of our <a className='link--primary' href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -1,133 +1,28 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import ProgressBar from 'components/ProgressBar';
|
||||
import Request from 'utils/request';
|
||||
import { LOCAL_CHECK, SEARCHING, UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states';
|
||||
import { connect } from 'react-redux';
|
||||
import View from './view';
|
||||
import { fileRequested } from 'actions/show';
|
||||
import { selectAsset } from 'selectors/show';
|
||||
|
||||
class AssetDisplay extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error : null,
|
||||
status: LOCAL_CHECK,
|
||||
};
|
||||
this.isLocalFileAvailableOnServer = this.isLocalFileAvailableOnServer.bind(this);
|
||||
this.triggerGetAssetOnServer = this.triggerGetAssetOnServer.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
const that = this;
|
||||
this.isLocalFileAvailableOnServer()
|
||||
.then(isAvailable => {
|
||||
if (!isAvailable) {
|
||||
console.log('file is not yet available');
|
||||
that.setState({status: SEARCHING});
|
||||
return that.triggerGetAssetOnServer();
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
that.setState({status: AVAILABLE});
|
||||
})
|
||||
.catch(error => {
|
||||
that.setState({
|
||||
status: UNAVAILABLE,
|
||||
error : error.message,
|
||||
});
|
||||
});
|
||||
}
|
||||
isLocalFileAvailableOnServer () {
|
||||
console.log(`checking if file is available for ${this.props.name}#${this.props.claimId}`);
|
||||
const url = `/api/file-is-available/${this.props.name}/${this.props.claimId}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
Request(url)
|
||||
.then(({success, message, data: isAvailable}) => {
|
||||
if (success) {
|
||||
console.log('/api/file-is-available response:', isAvailable);
|
||||
return resolve(isAvailable);
|
||||
}
|
||||
reject(new Error(message));
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
triggerGetAssetOnServer () {
|
||||
console.log(`getting claim for ${this.props.name}#${this.props.claimId}`);
|
||||
const url = `/api/claim-get/${this.props.name}/${this.props.claimId}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
Request(url)
|
||||
.then(({success, message}) => {
|
||||
console.log('/api/claim-get response:', success, message);
|
||||
if (success) {
|
||||
return resolve(true);
|
||||
}
|
||||
reject(new Error(message));
|
||||
})
|
||||
.catch(error => {
|
||||
reject(error);
|
||||
});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div id="asset-display-component">
|
||||
{(this.state.status === LOCAL_CHECK) &&
|
||||
<div>
|
||||
<p>Checking to see if Spee.ch has your asset locally...</p>
|
||||
</div>
|
||||
}
|
||||
{(this.state.status === SEARCHING) &&
|
||||
<div>
|
||||
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
|
||||
<ProgressBar size={12}/>
|
||||
<p>Curious what magic is happening here? <a className="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(this.state.status === UNAVAILABLE) &&
|
||||
<div>
|
||||
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the <a className="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">LBRY discord</a>.</p>
|
||||
<i><p id="error-message">{this.state.error}</p></i>
|
||||
</div>
|
||||
}
|
||||
{(this.state.status === AVAILABLE) &&
|
||||
(() => {
|
||||
switch (this.props.contentType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
return (
|
||||
<img className="asset" src={this.props.src} alt={this.props.name}/>
|
||||
);
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img className="asset" src={this.props.src} alt={this.props.name}/>
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video id="video" className="asset" controls poster={this.props.thumbnail}>
|
||||
<source src={this.props.src}/>
|
||||
<p>Your browser does not support the <code>video</code> element.</p>
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>Unsupported file type</p>
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select error and status
|
||||
const error = show.displayAsset.error;
|
||||
const status = show.displayAsset.status;
|
||||
// select asset
|
||||
const asset = selectAsset(show);
|
||||
// return props
|
||||
return {
|
||||
error,
|
||||
status,
|
||||
asset,
|
||||
};
|
||||
};
|
||||
|
||||
AssetDisplay.propTypes = {
|
||||
name : PropTypes.string.isRequired,
|
||||
claimId : PropTypes.string.isRequired,
|
||||
src : PropTypes.string.isRequired,
|
||||
contentType: PropTypes.string.isRequired,
|
||||
fileExt : PropTypes.string.isRequired,
|
||||
thumbnail : PropTypes.string,
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onFileRequest: (name, claimId) => {
|
||||
dispatch(fileRequested(name, claimId));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default AssetDisplay;
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
73
react/components/AssetDisplay/view.jsx
Normal file
73
react/components/AssetDisplay/view.jsx
Normal file
|
@ -0,0 +1,73 @@
|
|||
import React from 'react';
|
||||
import ProgressBar from 'components/ProgressBar';
|
||||
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from 'constants/asset_display_states';
|
||||
|
||||
class AssetDisplay extends React.Component {
|
||||
componentDidMount () {
|
||||
const { asset: { claimData: { name, claimId } } } = this.props;
|
||||
this.props.onFileRequest(name, claimId);
|
||||
}
|
||||
render () {
|
||||
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
|
||||
return (
|
||||
<div id='asset-display-component'>
|
||||
{(status === LOCAL_CHECK) &&
|
||||
<div>
|
||||
<p>Checking to see if Spee.ch has your asset locally...</p>
|
||||
</div>
|
||||
}
|
||||
{(status === UNAVAILABLE) &&
|
||||
<div>
|
||||
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
|
||||
<ProgressBar size={12} />
|
||||
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(status === ERROR) &&
|
||||
<div>
|
||||
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the <a className='link--primary' href='https://discord.gg/YjYbwhS' target='_blank'>LBRY discord</a>.</p>
|
||||
<i><p id='error-message'>{error}</p></i>
|
||||
</div>
|
||||
}
|
||||
{(status === AVAILABLE) &&
|
||||
(() => {
|
||||
switch (contentType) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
return (
|
||||
<img
|
||||
className='asset'
|
||||
src={`/${claimId}/${name}.${fileExt}`}
|
||||
alt={name} />
|
||||
);
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img
|
||||
className='asset'
|
||||
src={`/${claimId}/${name}.${fileExt}`}
|
||||
alt={name}
|
||||
/>
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video id='video' className='asset' controls poster={thumbnail}>
|
||||
<source
|
||||
src={`/${claimId}/${name}.${fileExt}`}
|
||||
/>
|
||||
<p>Your browser does not support the <code>video</code> element.</p>
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<p>Unsupported file type</p>
|
||||
);
|
||||
}
|
||||
})()
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AssetDisplay;
|
|
@ -1,177 +1,14 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { connect } from 'react-redux';
|
||||
import View from './view';
|
||||
import { selectAsset } from 'selectors/show';
|
||||
|
||||
class AssetInfo extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDetails: false,
|
||||
};
|
||||
this.toggleDetails = this.toggleDetails.bind(this);
|
||||
this.copyToClipboard = this.copyToClipboard.bind(this);
|
||||
}
|
||||
toggleDetails () {
|
||||
if (this.state.showDetails) {
|
||||
return this.setState({showDetails: false});
|
||||
}
|
||||
this.setState({showDetails: true});
|
||||
}
|
||||
copyToClipboard (event) {
|
||||
var elementToCopy = event.target.dataset.elementtocopy;
|
||||
var element = document.getElementById(elementToCopy);
|
||||
element.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
this.setState({error: 'Oops, unable to copy'});
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
{this.props.channelName &&
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Channel:</span>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<span className="text"><a href={`/${this.props.channelName}:${this.props.certificateId}`}>{this.props.channelName}</a></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{this.props.description &&
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<span className="text">{this.props.description}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div id="show-short-link">
|
||||
<div className="column column--2 column--med-10">
|
||||
<a className="link--primary" href={`/${this.props.shortClaimId}/${this.props.name}.${this.props.fileExt}`}><span
|
||||
className="text">Link:</span></a>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<div className="row row--short row--wide">
|
||||
<div className="column column--7">
|
||||
<div className="input-error" id="input-error-copy-short-link" hidden="true">error here</div>
|
||||
<input type="text" id="short-link" className="input-disabled input-text--full-width" readOnly
|
||||
spellCheck="false"
|
||||
value={`${this.props.host}/${this.props.shortClaimId}/${this.props.name}.${this.props.fileExt}`}
|
||||
onClick={this.select}/>
|
||||
</div>
|
||||
<div className="column column--1"> </div>
|
||||
<div className="column column--2">
|
||||
<button className="button--primary" data-elementtocopy="short-link"
|
||||
onClick={this.copyToClipboard}>copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-embed-code">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Embed:</span>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<div className="row row--short row--wide">
|
||||
<div className="column column--7">
|
||||
<div className="input-error" id="input-error-copy-embed-text" hidden="true">error here</div>
|
||||
{(this.props.contentType === 'video/mp4') ? (
|
||||
<input type="text" id="embed-text" className="input-disabled input-text--full-width" readOnly
|
||||
onClick={this.select} spellCheck="false"
|
||||
value={`<video width="100%" controls poster="${this.props.thumbnail}" src="${this.props.host}/${this.props.claimId}/${this.props.name}.${this.props.fileExt}"/></video>`}/>
|
||||
) : (
|
||||
<input type="text" id="embed-text" className="input-disabled input-text--full-width" readOnly
|
||||
onClick={this.select} spellCheck="false"
|
||||
value={`<img src="${this.props.host}/${this.props.claimId}/${this.props.name}.${this.props.fileExt}"/>`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="column column--1"> </div>
|
||||
<div className="column column--2">
|
||||
<button className="button--primary" data-elementtocopy="embed-text"
|
||||
onClick={this.copyToClipboard}>copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="show-share-buttons">
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Share:</span>
|
||||
</div>
|
||||
<div className="column column--7 column--med-10">
|
||||
<div
|
||||
className="row row--short row--wide flex-container--row flex-container--space-between-bottom flex-container--wrap">
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://twitter.com/intent/tweet?text=${this.props.host}/${this.props.shortClaimId}/${this.props.name}`}>twitter</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${this.props.host}/${this.props.shortClaimId}/${this.props.name}`}>facebook</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`http://tumblr.com/widgets/share/tool?canonicalUrl=${this.props.host}/${this.props.shortClaimId}/${this.props.name}`}>tumblr</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://www.reddit.com/submit?url=${this.props.host}/${this.props.shortClaimId}/${this.props.name}&title=${this.props.name}`}>reddit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ this.state.showDetails &&
|
||||
<div>
|
||||
<div className="row--padded row--wide row--no-top">
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Claim Name:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{this.props.name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Claim Id:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{this.props.claimId}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">File Type:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{this.props.contentType ? `${this.props.contentType}` : 'unknown'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row--padded row--wide row--no-top">
|
||||
<div className="column column--10">
|
||||
<a target="_blank" href="https://lbry.io/dmca">Report</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="row row--wide">
|
||||
<a className="text link--primary" id="show-details-toggle" href="#" onClick={this.toggleDetails}>{this.state.showDetails ? '[less]' : '[more]'}</a>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select asset
|
||||
const asset = selectAsset(show);
|
||||
// return props
|
||||
return {
|
||||
asset,
|
||||
};
|
||||
};
|
||||
|
||||
AssetInfo.propTypes = {
|
||||
channelName : PropTypes.string,
|
||||
certificateId: PropTypes.string,
|
||||
description : PropTypes.string,
|
||||
shortId : PropTypes.string.isRequired,
|
||||
name : PropTypes.string.isRequired,
|
||||
claimId : PropTypes.string.isRequired,
|
||||
contentType : PropTypes.string.isRequired,
|
||||
fileExt : PropTypes.string.isRequired,
|
||||
thumbnail : PropTypes.string,
|
||||
host : PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default AssetInfo;
|
||||
export default connect(mapStateToProps, null)(View);
|
||||
|
|
165
react/components/AssetInfo/view.jsx
Normal file
165
react/components/AssetInfo/view.jsx
Normal file
|
@ -0,0 +1,165 @@
|
|||
import React from 'react';
|
||||
import { Link } from 'react-router-dom';
|
||||
|
||||
class AssetInfo extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
showDetails: false,
|
||||
};
|
||||
this.toggleDetails = this.toggleDetails.bind(this);
|
||||
this.copyToClipboard = this.copyToClipboard.bind(this);
|
||||
}
|
||||
toggleDetails () {
|
||||
if (this.state.showDetails) {
|
||||
return this.setState({showDetails: false});
|
||||
}
|
||||
this.setState({showDetails: true});
|
||||
}
|
||||
copyToClipboard (event) {
|
||||
var elementToCopy = event.target.dataset.elementtocopy;
|
||||
var element = document.getElementById(elementToCopy);
|
||||
element.select();
|
||||
try {
|
||||
document.execCommand('copy');
|
||||
} catch (err) {
|
||||
this.setState({error: 'Oops, unable to copy'});
|
||||
}
|
||||
}
|
||||
render () {
|
||||
const { asset: { shortId, claimData : { channelName, certificateId, description, name, claimId, fileExt, contentType, thumbnail, host } } } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{channelName &&
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Channel:</span>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<span className="text"><Link to={`/${channelName}:${certificateId}`}>{channelName}</Link></span>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
{description &&
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<span className="text">{description}</span>
|
||||
</div>
|
||||
}
|
||||
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div id="show-short-link">
|
||||
<div className="column column--2 column--med-10">
|
||||
<Link className="link--primary" to={`/${shortId}/${name}.${fileExt}`}><span
|
||||
className="text">Link:</span></Link>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<div className="row row--short row--wide">
|
||||
<div className="column column--7">
|
||||
<div className="input-error" id="input-error-copy-short-link" hidden="true">error here</div>
|
||||
<input type="text" id="short-link" className="input-disabled input-text--full-width" readOnly
|
||||
spellCheck="false"
|
||||
value={`${host}/${shortId}/${name}.${fileExt}`}
|
||||
onClick={this.select}/>
|
||||
</div>
|
||||
<div className="column column--1"> </div>
|
||||
<div className="column column--2">
|
||||
<button className="button--primary" data-elementtocopy="short-link"
|
||||
onClick={this.copyToClipboard}>copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div id="show-embed-code">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Embed:</span>
|
||||
</div>
|
||||
<div className="column column--8 column--med-10">
|
||||
<div className="row row--short row--wide">
|
||||
<div className="column column--7">
|
||||
<div className="input-error" id="input-error-copy-embed-text" hidden="true">error here</div>
|
||||
{(contentType === 'video/mp4') ? (
|
||||
<input type="text" id="embed-text" className="input-disabled input-text--full-width" readOnly
|
||||
onClick={this.select} spellCheck="false"
|
||||
value={`<video width="100%" controls poster="${thumbnail}" src="${host}/${claimId}/${name}.${fileExt}"/></video>`}/>
|
||||
) : (
|
||||
<input type="text" id="embed-text" className="input-disabled input-text--full-width" readOnly
|
||||
onClick={this.select} spellCheck="false"
|
||||
value={`<img src="${host}/${claimId}/${name}.${fileExt}"/>`}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="column column--1"> </div>
|
||||
<div className="column column--2">
|
||||
<button className="button--primary" data-elementtocopy="embed-text"
|
||||
onClick={this.copyToClipboard}>copy
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="show-share-buttons">
|
||||
<div className="row row--padded row--wide row--no-top">
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Share:</span>
|
||||
</div>
|
||||
<div className="column column--7 column--med-10">
|
||||
<div
|
||||
className="row row--short row--wide flex-container--row flex-container--space-between-bottom flex-container--wrap">
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://twitter.com/intent/tweet?text=${host}/${shortId}/${name}`}>twitter</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://www.facebook.com/sharer/sharer.php?u=${host}/${shortId}/${name}`}>facebook</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`http://tumblr.com/widgets/share/tool?canonicalUrl=${host}/${shortId}/${name}`}>tumblr</a>
|
||||
<a className="link--primary" target="_blank"
|
||||
href={`https://www.reddit.com/submit?url=${host}/${shortId}/${name}&title=${name}`}>reddit</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{ this.state.showDetails &&
|
||||
<div>
|
||||
<div className="row--padded row--wide row--no-top">
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Claim Name:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{name}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">Claim Id:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{claimId}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<div className="column column--2 column--med-10">
|
||||
<span className="text">File Type:</span>
|
||||
</div><div className="column column--8 column--med-10">
|
||||
{contentType ? `${contentType}` : 'unknown'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="row--padded row--wide row--no-top">
|
||||
<div className="column column--10">
|
||||
<a target="_blank" href="https://lbry.io/dmca">Report</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
<div className="row row--wide">
|
||||
<button className="button--secondary" onClick={this.toggleDetails}>{this.state.showDetails ? 'less' : 'more'}</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default AssetInfo;
|
|
@ -3,23 +3,9 @@ import { Link } from 'react-router-dom';
|
|||
|
||||
const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
|
||||
const directSourceLink = `${claimId}/${name}.${fileExt}`;
|
||||
const showUrlLink = `${claimId}/${name}`;
|
||||
const previewHolderStyle = {
|
||||
clear : 'both',
|
||||
display : 'inline-block',
|
||||
width : '31%',
|
||||
padding : '0px',
|
||||
margin : '1%',
|
||||
backgroundColor: 'black',
|
||||
};
|
||||
const assetStyle = {
|
||||
width : '100%',
|
||||
padding: '0px',
|
||||
margin : '0px',
|
||||
display: 'block',
|
||||
};
|
||||
const showUrlLink = `/${claimId}/${name}`;
|
||||
return (
|
||||
<div style={previewHolderStyle}>
|
||||
<div className='asset-holder'>
|
||||
<Link to={showUrlLink} >
|
||||
{(() => {
|
||||
switch (contentType) {
|
||||
|
@ -27,16 +13,16 @@ const AssetPreview = ({ name, claimId, fileExt, contentType }) => {
|
|||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
return (
|
||||
<img style={assetStyle} className={'asset-preview--image'} src={directSourceLink} alt={name}/>
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name} />
|
||||
);
|
||||
case 'image/gif':
|
||||
return (
|
||||
<img style={assetStyle} className={'asset-preview--gif'} src={directSourceLink} alt={name}/>
|
||||
<img className={'asset-preview'} src={directSourceLink} alt={name} />
|
||||
);
|
||||
case 'video/mp4':
|
||||
return (
|
||||
<video style={assetStyle}>
|
||||
<source src={directSourceLink} type={contentType}/>
|
||||
<video className={'asset-preview'}>
|
||||
<source src={directSourceLink} type={contentType} />
|
||||
</video>
|
||||
);
|
||||
default:
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
import React from 'react';
|
||||
import { connect } from 'react-redux';
|
||||
import View from './view';
|
||||
import { selectAsset } from 'selectors/show';
|
||||
|
||||
const AssetTitle = ({title}) => {
|
||||
return (
|
||||
<div>
|
||||
<span className="text--large">{title}</span>
|
||||
</div>
|
||||
);
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select title
|
||||
const { claimData: { title } } = selectAsset(show);
|
||||
// return props
|
||||
return {
|
||||
title,
|
||||
};
|
||||
};
|
||||
|
||||
export default AssetTitle;
|
||||
export default connect(mapStateToProps, null)(View);
|
||||
|
|
11
react/components/AssetTitle/view.jsx
Normal file
11
react/components/AssetTitle/view.jsx
Normal file
|
@ -0,0 +1,11 @@
|
|||
import React from 'react';
|
||||
|
||||
const AssetTitle = ({ title }) => {
|
||||
return (
|
||||
<div>
|
||||
<span className="text--large">{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetTitle;
|
|
@ -1,20 +1,23 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import NavBar from 'containers/NavBar';
|
||||
|
||||
class ErrorPage extends React.Component {
|
||||
render () {
|
||||
const { error } = this.props;
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<p>{this.props.error}</p>
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<p>{error}</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
// required props
|
||||
// error
|
||||
ErrorPage.propTypes = {
|
||||
error: PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ErrorPage;
|
||||
|
|
24
react/components/FourOhFourPage/index.js
Normal file
24
react/components/FourOhFourPage/index.js
Normal file
|
@ -0,0 +1,24 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import Helmet from 'react-helmet';
|
||||
const { site: { title, host } } = require('../../../config/speechConfig.js');
|
||||
|
||||
class FourOhForPage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<Helmet>
|
||||
<title>{title} - 404</title>
|
||||
<link rel='canonical' href={`${host}/404`} />
|
||||
</Helmet>
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<h2>404</h2>
|
||||
<p>That page does not exist</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default FourOhForPage;
|
25
react/components/GAListener/index.js
Normal file
25
react/components/GAListener/index.js
Normal file
|
@ -0,0 +1,25 @@
|
|||
import React from 'react';
|
||||
import GoogleAnalytics from 'react-ga';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
const config = require('../../../config/speechConfig.js');
|
||||
const googleApiKey = config.analytics.googleId;
|
||||
|
||||
GoogleAnalytics.initialize(googleApiKey);
|
||||
|
||||
class GAListener extends React.Component {
|
||||
componentDidMount () {
|
||||
this.sendPageView(this.props.history.location);
|
||||
this.props.history.listen(this.sendPageView);
|
||||
}
|
||||
|
||||
sendPageView (location) {
|
||||
GoogleAnalytics.set({ page: location.pathname });
|
||||
GoogleAnalytics.pageview(location.pathname);
|
||||
}
|
||||
|
||||
render () {
|
||||
return this.props.children;
|
||||
}
|
||||
}
|
||||
|
||||
export default withRouter(GAListener);
|
|
@ -1,18 +1,20 @@
|
|||
import React from 'react';
|
||||
import SEO from 'components/SEO';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import PublishTool from 'containers/PublishTool';
|
||||
|
||||
class PublishPage extends React.Component {
|
||||
class HomePage extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className={'row row--tall flex-container--column'}>
|
||||
<NavBar/>
|
||||
<SEO />
|
||||
<NavBar />
|
||||
<div className={'row row--tall row--padded flex-container--column'}>
|
||||
<PublishTool/>
|
||||
<PublishTool />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default PublishPage;
|
||||
export default HomePage;
|
|
@ -8,7 +8,6 @@ class Preview extends React.Component {
|
|||
imgSource : '',
|
||||
defaultThumbnail: '/assets/img/video_thumb_default.png',
|
||||
};
|
||||
this.previewFile = this.previewFile.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.previewFile(this.props.file);
|
||||
|
@ -22,21 +21,20 @@ class Preview extends React.Component {
|
|||
}
|
||||
}
|
||||
previewFile (file) {
|
||||
const that = this;
|
||||
if (file.type !== 'video/mp4') {
|
||||
const previewReader = new FileReader();
|
||||
previewReader.readAsDataURL(file);
|
||||
previewReader.onloadend = function () {
|
||||
that.setState({imgSource: previewReader.result});
|
||||
previewReader.onloadend = () => {
|
||||
this.setState({imgSource: previewReader.result});
|
||||
};
|
||||
} else {
|
||||
that.setState({imgSource: (this.props.thumbnail || this.state.defaultThumbnail)});
|
||||
this.setState({imgSource: (this.props.thumbnail || this.state.defaultThumbnail)});
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<img
|
||||
id="asset-preview"
|
||||
id="dropzone-preview"
|
||||
src={this.state.imgSource}
|
||||
className={this.props.dimPreview ? 'dim' : ''}
|
||||
alt="publish preview"
|
||||
|
|
|
@ -5,39 +5,39 @@ import * as publishStates from 'constants/publish_claim_states';
|
|||
|
||||
function PublishStatus ({ status, message }) {
|
||||
return (
|
||||
<div className="row row--tall flex-container--column flex-container--center-center">
|
||||
<div className='row row--tall flex-container--column flex-container--center-center'>
|
||||
{(status === publishStates.LOAD_START) &&
|
||||
<div className="row align-content-center">
|
||||
<div className='row align-content-center'>
|
||||
<p>File is loading to server</p>
|
||||
<p className="blue">{message}</p>
|
||||
<p className='blue'>{message}</p>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.LOADING) &&
|
||||
<div>
|
||||
<div className="row align-content-center">
|
||||
<div className='row align-content-center'>
|
||||
<p>File is loading to server</p>
|
||||
<p className="blue">{message}</p>
|
||||
<p className='blue'>{message}</p>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.PUBLISHING) &&
|
||||
<div className="row align-content-center">
|
||||
<div className='row align-content-center'>
|
||||
<p>Upload complete. Your file is now being published on the blockchain...</p>
|
||||
<ProgressBar size={12}/>
|
||||
<p>Curious what magic is happening here? <a className="link--primary" target="blank" href="https://lbry.io/faq/what-is-lbry">Learn more.</a></p>
|
||||
<ProgressBar size={12} />
|
||||
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.SUCCESS) &&
|
||||
<div className="row align-content-center">
|
||||
<div className='row align-content-center'>
|
||||
<p>Your publish is complete! You are being redirected to it now.</p>
|
||||
<p>If you are not automatically redirected, <a className="link--primary" target="_blank" href={message}>click here.</a></p>
|
||||
<p>If you are not automatically redirected, <a className='link--primary' target='_blank' href={message}>click here.</a></p>
|
||||
</div>
|
||||
}
|
||||
{(status === publishStates.FAILED) &&
|
||||
<div className="row align-content-center">
|
||||
<div className='row align-content-center'>
|
||||
<p>Something went wrong...</p>
|
||||
<p><strong>{message}</strong></p>
|
||||
<p>For help, post the above error text in the #speech channel on the <a className="link--primary" href="https://discord.gg/YjYbwhS" target="_blank">lbry discord</a></p>
|
||||
<p>For help, post the above error text in the #speech channel on the <a className='link--primary' href='https://discord.gg/YjYbwhS' target='_blank'>lbry discord</a></p>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
|
32
react/components/SEO/index.jsx
Normal file
32
react/components/SEO/index.jsx
Normal 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;
|
|
@ -1,66 +1,21 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import AssetTitle from 'components/AssetTitle';
|
||||
import AssetDisplay from 'components/AssetDisplay';
|
||||
import AssetInfo from 'components/AssetInfo';
|
||||
import { connect } from 'react-redux';
|
||||
import View from './view';
|
||||
|
||||
class ShowAssetDetails extends React.Component {
|
||||
componentDidMount () {
|
||||
console.log('ShowAssetDetails props', this.props);
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
{this.props.error &&
|
||||
<div className="row row--padded">
|
||||
<p>{this.props.error}</p>
|
||||
</div>
|
||||
}
|
||||
{this.props.claimData &&
|
||||
<div className="row row--tall row--padded">
|
||||
<div className="column column--10">
|
||||
<AssetTitle title={this.props.claimData.title}/>
|
||||
</div>
|
||||
<div className="column column--5 column--sml-10 align-content-top">
|
||||
<div className="row row--padded">
|
||||
<AssetDisplay
|
||||
name={this.props.claimData.name}
|
||||
claimId={this.props.claimData.claimId}
|
||||
src={`/${this.props.claimData.claimId}/${this.props.claimData.name}.${this.props.claimData.fileExt}`}
|
||||
contentType={this.props.claimData.contentType}
|
||||
fileExt={this.props.claimData.fileExt}
|
||||
thumbnail={this.props.claimData.thumbnail}
|
||||
/>
|
||||
</div>
|
||||
</div><div className="column column--5 column--sml-10 align-content-top">
|
||||
<div className="row row--padded">
|
||||
<AssetInfo
|
||||
channelName={this.props.claimData.channelName}
|
||||
certificateId={this.props.claimData.certificateId}
|
||||
description={this.props.claimData.description}
|
||||
name={this.props.claimData.name}
|
||||
claimId={this.props.claimData.claimId}
|
||||
fileExt={this.props.claimData.fileExt}
|
||||
contentType={this.props.claimData.contentType}
|
||||
thumbnail={this.props.claimData.thumbnail}
|
||||
host={this.props.claimData.host}
|
||||
shortClaimId={this.props.shortId}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select request info
|
||||
const requestId = show.request.id;
|
||||
// select asset info
|
||||
let asset;
|
||||
const request = show.requestList[requestId] || null;
|
||||
const assetList = show.assetList;
|
||||
if (request && assetList) {
|
||||
const assetKey = request.key; // note: just store this in the request
|
||||
asset = assetList[assetKey] || null;
|
||||
};
|
||||
// return props
|
||||
return {
|
||||
asset,
|
||||
};
|
||||
};
|
||||
|
||||
ShowAssetDetails.propTypes = {
|
||||
error : PropTypes.string,
|
||||
claimData: PropTypes.object.isRequired,
|
||||
shortId : PropTypes.string.isRequired,
|
||||
};
|
||||
|
||||
export default ShowAssetDetails;
|
||||
export default connect(mapStateToProps, null)(View);
|
||||
|
|
42
react/components/ShowAssetDetails/view.jsx
Normal file
42
react/components/ShowAssetDetails/view.jsx
Normal file
|
@ -0,0 +1,42 @@
|
|||
import React from 'react';
|
||||
import SEO from 'components/SEO';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import ErrorPage from 'components/ErrorPage';
|
||||
import AssetTitle from 'components/AssetTitle';
|
||||
import AssetDisplay from 'components/AssetDisplay';
|
||||
import AssetInfo from 'components/AssetInfo';
|
||||
|
||||
class ShowAssetDetails extends React.Component {
|
||||
render () {
|
||||
const { asset } = this.props;
|
||||
if (asset) {
|
||||
const { name } = asset.claimData;
|
||||
return (
|
||||
<div>
|
||||
<SEO pageTitle={`${name} - details`} asset={asset} />
|
||||
<NavBar />
|
||||
<div className='row row--tall row--padded'>
|
||||
<div className='column column--10'>
|
||||
<AssetTitle />
|
||||
</div>
|
||||
<div className='column column--5 column--sml-10 align-content-top'>
|
||||
<div className='row row--padded show-details-container'>
|
||||
<AssetDisplay />
|
||||
</div>
|
||||
</div><div className='column column--5 column--sml-10 align-content-top'>
|
||||
<div className='row row--padded'>
|
||||
<AssetInfo />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ErrorPage error={'loading asset data...'} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowAssetDetails;
|
|
@ -1,36 +1,21 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Link } from 'react-router-dom';
|
||||
import AssetDisplay from 'components/AssetDisplay';
|
||||
import { connect } from 'react-redux';
|
||||
import View from './view';
|
||||
|
||||
class ShowLite extends React.Component {
|
||||
render () {
|
||||
return (
|
||||
<div className="row row--tall flex-container--column flex-container--center-center">
|
||||
{this.props.error &&
|
||||
<p>{this.props.error}</p>
|
||||
}
|
||||
{this.props.claimData &&
|
||||
<div>
|
||||
<AssetDisplay
|
||||
name={this.props.claimData.name}
|
||||
claimId={this.props.claimData.claimId}
|
||||
src={`/${this.props.claimData.claimId}/${this.props.claimData.name}.${this.props.claimData.fileExt}`}
|
||||
contentType={this.props.claimData.contentType}
|
||||
fileExt={this.props.claimData.fileExt}
|
||||
thumbnail={this.props.claimData.thumbnail}
|
||||
/>
|
||||
<Link id="asset-boilerpate" className="link--primary fine-print" to={`/${this.props.claimData.claimId}/${this.props.claimData.name}`}>hosted via Spee.ch</Link>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select request info
|
||||
const requestId = show.request.id;
|
||||
// select asset info
|
||||
let asset;
|
||||
const request = show.requestList[requestId] || null;
|
||||
const assetList = show.assetList;
|
||||
if (request && assetList) {
|
||||
const assetKey = request.key; // note: just store this in the request
|
||||
asset = assetList[assetKey] || null;
|
||||
};
|
||||
// return props
|
||||
return {
|
||||
asset,
|
||||
};
|
||||
};
|
||||
|
||||
ShowLite.propTypes = {
|
||||
error : PropTypes.string,
|
||||
claimData: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default ShowLite;
|
||||
export default connect(mapStateToProps, null)(View);
|
||||
|
|
28
react/components/ShowAssetLite/view.jsx
Normal file
28
react/components/ShowAssetLite/view.jsx
Normal file
|
@ -0,0 +1,28 @@
|
|||
import React from 'react';
|
||||
import SEO from 'components/SEO';
|
||||
import { Link } from 'react-router-dom';
|
||||
import AssetDisplay from 'components/AssetDisplay';
|
||||
|
||||
class ShowLite extends React.Component {
|
||||
render () {
|
||||
const { asset } = this.props;
|
||||
if (asset) {
|
||||
const { name, claimId } = asset.claimData;
|
||||
return (
|
||||
<div className='row row--tall flex-container--column flex-container--center-center show-lite-container'>
|
||||
<SEO pageTitle={name} asset={asset} />
|
||||
<AssetDisplay />
|
||||
<Link id='asset-boilerpate' className='link--primary fine-print' to={`/${claimId}/${name}`}>hosted
|
||||
via Spee.ch</Link>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className='row row--tall row--padded flex-container--column flex-container--center-center'>
|
||||
<p>loading asset data...</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowLite;
|
20
react/components/ShowChannel/index.js
Normal file
20
react/components/ShowChannel/index.js
Normal file
|
@ -0,0 +1,20 @@
|
|||
import { connect } from 'react-redux';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select request info
|
||||
const requestId = show.request.id;
|
||||
// select request
|
||||
const previousRequest = show.requestList[requestId] || null;
|
||||
// select channel
|
||||
let channel;
|
||||
if (previousRequest) {
|
||||
const channelKey = previousRequest.key;
|
||||
channel = show.channelList[channelKey] || null;
|
||||
}
|
||||
return {
|
||||
channel,
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, null)(View);
|
35
react/components/ShowChannel/view.jsx
Normal file
35
react/components/ShowChannel/view.jsx
Normal file
|
@ -0,0 +1,35 @@
|
|||
import React from 'react';
|
||||
import SEO from 'components/SEO';
|
||||
import ErrorPage from 'components/ErrorPage';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
|
||||
|
||||
class ShowChannel extends React.Component {
|
||||
render () {
|
||||
const { channel } = this.props;
|
||||
if (channel) {
|
||||
const { name, longId, shortId } = channel;
|
||||
return (
|
||||
<div>
|
||||
<SEO pageTitle={name} channel={channel} />
|
||||
<NavBar />
|
||||
<div className='row row--tall row--padded'>
|
||||
<div className='column column--10'>
|
||||
<h2>channel name: {name}</h2>
|
||||
<p className={'fine-print'}>full channel id: {longId}</p>
|
||||
<p className={'fine-print'}>short channel id: {shortId}</p>
|
||||
</div>
|
||||
<div className='column column--10'>
|
||||
<ChannelClaimsDisplay />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<ErrorPage error={'loading channel data...'} />
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowChannel;
|
|
@ -1,4 +1,4 @@
|
|||
export const LOCAL_CHECK = 'LOCAL_CHECK';
|
||||
export const SEARCHING = 'SEARCHING';
|
||||
export const UNAVAILABLE = 'UNAVAILABLE';
|
||||
export const ERROR = 'ERROR';
|
||||
export const AVAILABLE = 'AVAILABLE';
|
||||
|
|
|
@ -1,5 +1,21 @@
|
|||
export const REQUEST_UPDATE_CHANNEL = 'REQUEST_UPDATE_CHANNEL';
|
||||
export const REQUEST_UPDATE_CLAIM = 'REQUEST_UPDATE_CLAIM';
|
||||
export const CHANNEL_DATA_UPDATE = 'CHANNEL_DATA_UPDATE';
|
||||
export const CHANNEL_CLAIMS_DATA_UPDATE = 'CHANNEL_CLAIMS_DATA_UPDATE';
|
||||
export const ASSET_CLAIM_DATA_UPDATE = 'ASSET_CLAIM_DATA_UPDATE';
|
||||
// request actions
|
||||
export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI';
|
||||
export const REQUEST_ERROR = 'REQUEST_ERROR';
|
||||
export const REQUEST_UPDATE = 'REQUEST_UPDATE';
|
||||
export const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW';
|
||||
export const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW';
|
||||
export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
|
||||
|
||||
// asset actions
|
||||
export const ASSET_ADD = `ASSET_ADD`;
|
||||
|
||||
// channel actions
|
||||
export const CHANNEL_ADD = 'CHANNEL_ADD';
|
||||
|
||||
export const CHANNEL_CLAIMS_UPDATE_ASYNC = 'CHANNEL_CLAIMS_UPDATE_ASYNC';
|
||||
export const CHANNEL_CLAIMS_UPDATE_SUCCESS = 'CHANNEL_CLAIMS_UPDATE_SUCCESS';
|
||||
|
||||
// asset/file display actions
|
||||
export const FILE_REQUESTED = 'FILE_REQUESTED';
|
||||
export const FILE_AVAILABILITY_UPDATE = 'FILE_AVAILABILITY_UPDATE';
|
||||
export const DISPLAY_ASSET_ERROR = 'DISPLAY_ASSET_ERROR';
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
export const CHANNEL = 'CHANNEL';
|
||||
export const ASSET = 'ASSET';
|
||||
export const ASSET_LITE = 'ASSET_LITE';
|
||||
export const ASSET_DETAILS = 'ASSET_DETAILS';
|
||||
|
|
|
@ -1,27 +1,22 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { updateChannelClaimsData } from 'actions/show';
|
||||
import { onUpdateChannelClaims } from 'actions/show';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
// select channel key
|
||||
const request = show.requestList[show.request.id];
|
||||
const channelKey = request.key;
|
||||
// select channel claims
|
||||
const channel = show.channelList[channelKey] || null;
|
||||
// return props
|
||||
return {
|
||||
name : show.showChannel.channelData.name,
|
||||
longId : show.showChannel.channelData.longId,
|
||||
claims : show.showChannel.channelClaimsData.claims,
|
||||
currentPage: show.showChannel.channelClaimsData.currentPage,
|
||||
totalPages : show.showChannel.channelClaimsData.totalPages,
|
||||
totalClaims: show.showChannel.channelClaimsData.totalClaims,
|
||||
channelKey,
|
||||
channel,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChannelClaimsDataUpdate: (claims, currentPage, totalPages, totalClaims) => {
|
||||
dispatch(updateChannelClaimsData(claims, currentPage, totalPages, totalClaims));
|
||||
},
|
||||
onChannelClaimsDataClear: () => {
|
||||
dispatch(updateChannelClaimsData(null, null, null, null));
|
||||
},
|
||||
};
|
||||
const mapDispatchToProps = {
|
||||
onUpdateChannelClaims,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -1,81 +1,50 @@
|
|||
import React from 'react/index';
|
||||
import AssetPreview from 'components/AssetPreview/index';
|
||||
import request from 'utils/request';
|
||||
import React from 'react';
|
||||
import AssetPreview from 'components/AssetPreview';
|
||||
|
||||
class ChannelClaimsDisplay extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
this.updateClaimsData = this.updateClaimsData.bind(this);
|
||||
this.showPreviousResultsPage = this.showPreviousResultsPage.bind(this);
|
||||
this.showNextResultsPage = this.showNextResultsPage.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
const name = this.props.name;
|
||||
const longId = this.props.longId;
|
||||
this.updateClaimsData(name, longId, 1);
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.name !== this.props.name || nextProps.longId !== this.props.longId) {
|
||||
this.updateClaimsData(nextProps.name, nextProps.longId, 1);
|
||||
}
|
||||
}
|
||||
updateClaimsData (name, longId, page) {
|
||||
const url = `/api/channel-claims/${name}/${longId}/${page}`;
|
||||
const that = this;
|
||||
return request(url)
|
||||
.then(({ success, message, data }) => {
|
||||
console.log('api/channel-claims response:', data);
|
||||
if (!success) {
|
||||
return that.setState({error: message});
|
||||
}
|
||||
that.setState({error: null}); // move this error to redux state
|
||||
that.props.onChannelClaimsDataUpdate(data.claims, data.currentPage, data.totalPages, data.totalResults);
|
||||
})
|
||||
.catch((error) => {
|
||||
that.setState({error: error.message});
|
||||
});
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.onChannelClaimsDataClear();
|
||||
this.showPreviousResultsPage = this.showPreviousResultsPage.bind(this);
|
||||
}
|
||||
showPreviousResultsPage () {
|
||||
const previousPage = parseInt(this.props.currentPage) - 1;
|
||||
this.updateClaimsData(this.props.name, this.props.longId, previousPage);
|
||||
const { channel: { claimsData: { currentPage } } } = this.props;
|
||||
const previousPage = parseInt(currentPage) - 1;
|
||||
this.showNewPage(previousPage);
|
||||
}
|
||||
showNextResultsPage () {
|
||||
const nextPage = parseInt(this.props.currentPage) + 1;
|
||||
this.updateClaimsData(this.props.name, this.props.longId, nextPage);
|
||||
const { channel: { claimsData: { currentPage } } } = this.props;
|
||||
const nextPage = parseInt(currentPage) + 1;
|
||||
this.showNewPage(nextPage);
|
||||
}
|
||||
showNewPage (page) {
|
||||
const { channelKey, channel: { name, longId } } = this.props;
|
||||
this.props.onUpdateChannelClaims(channelKey, name, longId, page);
|
||||
}
|
||||
render () {
|
||||
const { channel: { claimsData: { claims, currentPage, totalPages } } } = this.props;
|
||||
return (
|
||||
<div>
|
||||
{this.state.error ? (
|
||||
<div className="row">
|
||||
<div className="column column--10">
|
||||
<p>{this.state.error}</p>
|
||||
<div className="row row--tall">
|
||||
{(claims.length > 0) ? (
|
||||
<div>
|
||||
{claims.map((claim, index) => <AssetPreview
|
||||
name={claim.name}
|
||||
claimId={claim.claimId}
|
||||
fileExt={claim.fileExt}
|
||||
contentType={claim.contentType}
|
||||
key={`${claim.name}-${index}`}
|
||||
/>)}
|
||||
<div>
|
||||
{(currentPage > 1) &&
|
||||
<button className={'button--secondary'} onClick={this.showPreviousResultsPage}>Previous Page</button>
|
||||
}
|
||||
{(currentPage < totalPages) &&
|
||||
<button className={'button--secondary'} onClick={this.showNextResultsPage}>Next Page</button>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row row--tall">
|
||||
{this.props.claims &&
|
||||
<div>
|
||||
{this.props.claims.map((claim, index) => <AssetPreview
|
||||
name={claim.name}
|
||||
claimId={claim.claimId}
|
||||
fileExt={claim.fileExt}
|
||||
contentType={claim.contentType}
|
||||
key={`${claim.name}-${index}`}
|
||||
/>)}
|
||||
<div>
|
||||
{(this.props.currentPage > 1) && <button onClick={this.showPreviousResultsPage}>Previous Page</button>}
|
||||
{(this.props.currentPage < this.props.totalPages) && <button onClick={this.showNextResultsPage}>Next Page</button>}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
<p>There are no claims in this channel</p>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -11,13 +11,8 @@ class ChannelCreateForm extends React.Component {
|
|||
password: '',
|
||||
status : null,
|
||||
};
|
||||
this.cleanseChannelInput = this.cleanseChannelInput.bind(this);
|
||||
this.handleChannelInput = this.handleChannelInput.bind(this);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.updateIsChannelAvailable = this.updateIsChannelAvailable.bind(this);
|
||||
this.checkIsChannelAvailable = this.checkIsChannelAvailable.bind(this);
|
||||
this.checkIsPasswordProvided = this.checkIsPasswordProvided.bind(this);
|
||||
this.makePublishChannelRequest = this.makePublishChannelRequest.bind(this);
|
||||
this.createChannel = this.createChannel.bind(this);
|
||||
}
|
||||
cleanseChannelInput (input) {
|
||||
|
@ -41,24 +36,23 @@ class ChannelCreateForm extends React.Component {
|
|||
this.setState({[name]: value});
|
||||
}
|
||||
updateIsChannelAvailable (channel) {
|
||||
const that = this;
|
||||
const channelWithAtSymbol = `@${channel}`;
|
||||
request(`/api/channel-is-available/${channelWithAtSymbol}`)
|
||||
request(`/api/channel/availability/${channelWithAtSymbol}`)
|
||||
.then(isAvailable => {
|
||||
if (isAvailable) {
|
||||
that.setState({'error': null});
|
||||
this.setState({'error': null});
|
||||
} else {
|
||||
that.setState({'error': 'That channel has already been claimed'});
|
||||
this.setState({'error': 'That channel has already been claimed'});
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
that.setState({'error': error.message});
|
||||
this.setState({'error': error.message});
|
||||
});
|
||||
}
|
||||
checkIsChannelAvailable (channel) {
|
||||
const channelWithAtSymbol = `@${channel}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
request(`/api/channel-is-available/${channelWithAtSymbol}`)
|
||||
request(`/api/channel/availability/${channelWithAtSymbol}`)
|
||||
.then(isAvailable => {
|
||||
console.log('checkIsChannelAvailable result:', isAvailable);
|
||||
if (!isAvailable) {
|
||||
|
@ -105,21 +99,20 @@ class ChannelCreateForm extends React.Component {
|
|||
}
|
||||
createChannel (event) {
|
||||
event.preventDefault();
|
||||
const that = this;
|
||||
this.checkIsPasswordProvided()
|
||||
.then(() => {
|
||||
return that.checkIsChannelAvailable(that.state.channel, that.state.password);
|
||||
return this.checkIsChannelAvailable(this.state.channel, this.state.password);
|
||||
})
|
||||
.then(() => {
|
||||
that.setState({status: 'We are publishing your new channel. Sit tight...'});
|
||||
return that.makePublishChannelRequest(that.state.channel, that.state.password);
|
||||
this.setState({status: 'We are publishing your new channel. Sit tight...'});
|
||||
return this.makePublishChannelRequest(this.state.channel, this.state.password);
|
||||
})
|
||||
.then(result => {
|
||||
that.setState({status: null});
|
||||
that.props.onChannelLogin(result.channelName, result.shortChannelId, result.channelClaimId);
|
||||
this.setState({status: null});
|
||||
this.props.onChannelLogin(result.channelName, result.shortChannelId, result.channelClaimId);
|
||||
})
|
||||
.catch((error) => {
|
||||
that.setState({'error': error.message, status: null});
|
||||
this.setState({'error': error.message, status: null});
|
||||
});
|
||||
}
|
||||
render () {
|
||||
|
|
|
@ -27,22 +27,21 @@ class ChannelLoginForm extends React.Component {
|
|||
}),
|
||||
credentials: 'include',
|
||||
}
|
||||
const that = this;
|
||||
request('login', params)
|
||||
.then(({success, channelName, shortChannelId, channelClaimId, message}) => {
|
||||
console.log('loginToChannel success:', success);
|
||||
if (success) {
|
||||
that.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
|
||||
this.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
|
||||
} else {
|
||||
that.setState({'error': message});
|
||||
this.setState({'error': message});
|
||||
};
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('login error', error);
|
||||
if (error.message) {
|
||||
that.setState({'error': error.message});
|
||||
this.setState({'error': error.message});
|
||||
} else {
|
||||
that.setState({'error': error});
|
||||
this.setState({'error': error});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -22,6 +22,6 @@ const mapDispatchToProps = dispatch => {
|
|||
dispatch(updateSelectedChannel(value));
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { selectFile, updateError } from 'actions/publish';
|
||||
import { selectFile, updateError, clearFile } from 'actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ publish }) => {
|
||||
|
@ -12,13 +12,17 @@ const mapStateToProps = ({ publish }) => {
|
|||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onFileSelect: (file) => {
|
||||
selectFile: (file) => {
|
||||
dispatch(selectFile(file));
|
||||
dispatch(updateError('publishSubmit', null));
|
||||
},
|
||||
onFileError: (value) => {
|
||||
setFileError: (value) => {
|
||||
dispatch(clearFile());
|
||||
dispatch(updateError('file', value));
|
||||
},
|
||||
clearFileError: () => {
|
||||
dispatch(updateError('file', null));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
|
|
|
@ -74,20 +74,20 @@ class Dropzone extends React.Component {
|
|||
try {
|
||||
validateFile(file); // validate the file's name, type, and size
|
||||
} catch (error) {
|
||||
return this.props.onFileError(error.message);
|
||||
return this.props.setFileError(error.message);
|
||||
}
|
||||
// stage it so it will be ready when the publish button is clicked
|
||||
this.props.onFileError(null);
|
||||
this.props.onFileSelect(file);
|
||||
this.props.clearFileError(null);
|
||||
this.props.selectFile(file);
|
||||
}
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="row row--tall flex-container--column">
|
||||
<div className='row row--tall flex-container--column'>
|
||||
<form>
|
||||
<input className="input-file" type="file" id="file_input" name="file_input" accept="video/*,image/*" onChange={this.handleFileInput} encType="multipart/form-data"/>
|
||||
<input className='input-file' type='file' id='file_input' name='file_input' accept='video/*,image/*' onChange={this.handleFileInput} encType='multipart/form-data' />
|
||||
</form>
|
||||
<div id="preview-dropzone" className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
|
||||
<div id='preview-dropzone' className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
|
||||
{this.props.file ? (
|
||||
<div>
|
||||
<Preview
|
||||
|
@ -95,38 +95,38 @@ class Dropzone extends React.Component {
|
|||
file={this.props.file}
|
||||
thumbnail={this.props.thumbnail}
|
||||
/>
|
||||
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
|
||||
{ this.state.dragOver ? (
|
||||
<div id="dropzone-dragover">
|
||||
<p className="blue">Drop it.</p>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
{ this.state.mouseOver ? (
|
||||
<div id="dropzone-instructions">
|
||||
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
|
||||
<p>Drag & drop image or video here to publish</p>
|
||||
<p className="fine-print">OR</p>
|
||||
<p className="blue--underlined">CHOOSE FILE</p>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
<div id='dropzone-text-holder' className={'flex-container--column flex-container--center-center'}>
|
||||
{ this.state.dragOver ? (
|
||||
<div id='dropzone-dragover'>
|
||||
<p className='blue'>Drop it.</p>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
{ this.state.mouseOver ? (
|
||||
<div id='dropzone-instructions'>
|
||||
<p className='info-message-placeholder info-message--failure' id='input-error-file-selection'>{this.props.fileError}</p>
|
||||
<p>Drag & drop image or video here to publish</p>
|
||||
<p className='fine-print'>OR</p>
|
||||
<p className='blue--underlined'>CHOOSE FILE</p>
|
||||
</div>
|
||||
) : (
|
||||
null
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div id="dropzone-text-holder" className={'flex-container--column flex-container--center-center'}>
|
||||
<div id='dropzone-text-holder' className={'flex-container--column flex-container--center-center'}>
|
||||
{ this.state.dragOver ? (
|
||||
<div id="dropzone-dragover">
|
||||
<p className="blue">Drop it.</p>
|
||||
<div id='dropzone-dragover'>
|
||||
<p className='blue'>Drop it.</p>
|
||||
</div>
|
||||
) : (
|
||||
<div id="dropzone-instructions">
|
||||
<p className="info-message-placeholder info-message--failure" id="input-error-file-selection">{this.props.fileError}</p>
|
||||
<div id='dropzone-instructions'>
|
||||
<p className='info-message-placeholder info-message--failure' id='input-error-file-selection'>{this.props.fileError}</p>
|
||||
<p>Drag & drop image or video here to publish</p>
|
||||
<p className="fine-print">OR</p>
|
||||
<p className="blue--underlined">CHOOSE FILE</p>
|
||||
<p className='fine-print'>OR</p>
|
||||
<p className='blue--underlined'>CHOOSE FILE</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import React from 'react';
|
||||
import { withRouter } from 'react-router-dom';
|
||||
import SEO from 'components/SEO';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import ChannelLoginForm from 'containers/ChannelLoginForm';
|
||||
import ChannelCreateForm from 'containers/ChannelCreateForm';
|
||||
|
||||
class PublishPage extends React.Component {
|
||||
class LoginPage extends React.Component {
|
||||
componentWillReceiveProps (newProps) {
|
||||
// re-route the user to the homepage if the user is logged in
|
||||
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
|
||||
|
@ -15,24 +16,25 @@ class PublishPage extends React.Component {
|
|||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
<div className="row row--padded">
|
||||
<div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're <a className="link--primary" target="_blank" href="/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8">documenting important events</a>, or making a public repository for <a className="link--primary" target="_blank" href="/@catGifs">cat gifs</a> (password: '1234'), try creating a channel for it!</p>
|
||||
<SEO pageTitle={'Login'} pageUri={'login'} />
|
||||
<NavBar />
|
||||
<div className='row row--padded'>
|
||||
<div className='column column--5 column--med-10 align-content-top'>
|
||||
<div className='column column--8 column--med-10'>
|
||||
<p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're <a className='link--primary' target='_blank' href='/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8'>documenting important events</a>, or making a public repository for <a className='link--primary' target='_blank' href='/@catGifs'>cat gifs</a> (password: '1234'), try creating a channel for it!</p>
|
||||
</div>
|
||||
</div><div className='column column--5 column--med-10 align-content-top'>
|
||||
<div className='column column--8 column--med-10'>
|
||||
<h3 className='h3--no-bottom'>Log in to an existing channel:</h3>
|
||||
<ChannelLoginForm />
|
||||
<h3 className='h3--no-bottom'>Create a brand new channel:</h3>
|
||||
<ChannelCreateForm />
|
||||
</div>
|
||||
</div><div className="column column--5 column--med-10 align-content-top">
|
||||
<div className="column column--8 column--med-10">
|
||||
<h3 className="h3--no-bottom">Log in to an existing channel:</h3>
|
||||
<ChannelLoginForm />
|
||||
<h3 className="h3--no-bottom">Create a brand new channel:</h3>
|
||||
<ChannelCreateForm />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default withRouter(PublishPage);
|
||||
export default withRouter(LoginPage);
|
||||
|
|
|
@ -21,29 +21,21 @@ class NavBar extends React.Component {
|
|||
checkForLoggedInUser () {
|
||||
const params = {credentials: 'include'};
|
||||
request('/user', params)
|
||||
.then(({success, message, data}) => {
|
||||
if (success) {
|
||||
this.props.onChannelLogin(data.channelName, data.shortChannelId, data.channelClaimId);
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
.then(({ data }) => {
|
||||
this.props.onChannelLogin(data.channelName, data.shortChannelId, data.channelClaimId);
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('request encountered an error', error);
|
||||
console.log('/user error:', error.message);
|
||||
});
|
||||
}
|
||||
logoutUser () {
|
||||
const params = {credentials: 'include'};
|
||||
request('/logout', params)
|
||||
.then(({success, message}) => {
|
||||
if (success) {
|
||||
this.props.onChannelLogout();
|
||||
} else {
|
||||
console.log(message);
|
||||
}
|
||||
.then(() => {
|
||||
this.props.onChannelLogout();
|
||||
})
|
||||
.catch(error => {
|
||||
console.log('request encountered an error', error);
|
||||
console.log('/logout error', error.message);
|
||||
});
|
||||
}
|
||||
handleSelection (event) {
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import {connect} from 'react-redux';
|
||||
import {clearFile, selectFile, updateError, updatePublishStatus} from 'actions/publish';
|
||||
import {updateLoggedInChannel} from 'actions/channel';
|
||||
import {clearFile, updateError, updatePublishStatus} from 'actions/publish';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ channel, publish }) => {
|
||||
|
@ -23,15 +22,9 @@ const mapStateToProps = ({ channel, publish }) => {
|
|||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onFileSelect: (file) => {
|
||||
dispatch(selectFile(file));
|
||||
},
|
||||
onFileClear: () => {
|
||||
dispatch(clearFile());
|
||||
},
|
||||
onChannelLogin: (name, shortId, longId) => {
|
||||
dispatch(updateLoggedInChannel(name, shortId, longId));
|
||||
},
|
||||
onPublishStatusChange: (status, message) => {
|
||||
dispatch(updatePublishStatus(status, message));
|
||||
},
|
||||
|
|
|
@ -11,9 +11,7 @@ import * as publishStates from 'constants/publish_claim_states';
|
|||
class PublishForm extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.validateChannelSelection = this.validateChannelSelection.bind(this);
|
||||
this.validatePublishParams = this.validatePublishParams.bind(this);
|
||||
this.makePublishRequest = this.makePublishRequest.bind(this);
|
||||
// this.makePublishRequest = this.makePublishRequest.bind(this);
|
||||
this.publish = this.publish.bind(this);
|
||||
}
|
||||
validateChannelSelection () {
|
||||
|
@ -50,35 +48,33 @@ class PublishForm extends React.Component {
|
|||
}
|
||||
makePublishRequest (file, metadata) {
|
||||
console.log('making publish request');
|
||||
const uri = '/api/claim-publish';
|
||||
const uri = '/api/claim/publish';
|
||||
const xhr = new XMLHttpRequest();
|
||||
const fd = this.appendDataToFormData(file, metadata);
|
||||
const that = this;
|
||||
xhr.upload.addEventListener('loadstart', function () {
|
||||
that.props.onPublishStatusChange(publishStates.LOAD_START, 'upload started');
|
||||
xhr.upload.addEventListener('loadstart', () => {
|
||||
this.props.onPublishStatusChange(publishStates.LOAD_START, 'upload started');
|
||||
});
|
||||
xhr.upload.addEventListener('progress', function (e) {
|
||||
xhr.upload.addEventListener('progress', (e) => {
|
||||
if (e.lengthComputable) {
|
||||
const percentage = Math.round((e.loaded * 100) / e.total);
|
||||
console.log('progress:', percentage);
|
||||
that.props.onPublishStatusChange(publishStates.LOADING, `${percentage}%`);
|
||||
this.props.onPublishStatusChange(publishStates.LOADING, `${percentage}%`);
|
||||
}
|
||||
}, false);
|
||||
xhr.upload.addEventListener('load', function () {
|
||||
xhr.upload.addEventListener('load', () => {
|
||||
console.log('loaded 100%');
|
||||
that.props.onPublishStatusChange(publishStates.PUBLISHING, null);
|
||||
this.props.onPublishStatusChange(publishStates.PUBLISHING, null);
|
||||
}, false);
|
||||
xhr.open('POST', uri, true);
|
||||
xhr.onreadystatechange = function () {
|
||||
xhr.onreadystatechange = () => {
|
||||
if (xhr.readyState === 4) {
|
||||
const response = JSON.parse(xhr.response);
|
||||
console.log('publish response:', response);
|
||||
if ((xhr.status === 200) && response.success) {
|
||||
that.props.onPublishStatusChange(publishStates.SUCCESS, response.data.url);
|
||||
// redirect to the published asset's show page
|
||||
that.props.history.push(`/${response.data.claimId}/${response.data.name}`);
|
||||
this.props.history.push(`/${response.data.claimId}/${response.data.name}`);
|
||||
this.props.onFileClear();
|
||||
} else {
|
||||
that.props.onPublishStatusChange(publishStates.FAILED, response.message);
|
||||
this.props.onPublishStatusChange(publishStates.FAILED, response.message);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -106,7 +102,6 @@ class PublishForm extends React.Component {
|
|||
fd.append('file', file);
|
||||
for (var key in metadata) {
|
||||
if (metadata.hasOwnProperty(key)) {
|
||||
console.log('adding form data', key, metadata[key]);
|
||||
fd.append(key, metadata[key]);
|
||||
}
|
||||
}
|
||||
|
@ -115,60 +110,56 @@ class PublishForm extends React.Component {
|
|||
publish () {
|
||||
console.log('publishing file');
|
||||
// publish the asset
|
||||
const that = this;
|
||||
this.validateChannelSelection()
|
||||
.then(() => {
|
||||
return that.validatePublishParams();
|
||||
return this.validatePublishParams();
|
||||
})
|
||||
.then(() => {
|
||||
const metadata = that.createMetadata();
|
||||
const metadata = this.createMetadata();
|
||||
// publish the claim
|
||||
return that.makePublishRequest(that.props.file, metadata);
|
||||
})
|
||||
.then(() => {
|
||||
that.props.onPublishStatusChange('publish request made');
|
||||
return this.makePublishRequest(this.props.file, metadata);
|
||||
})
|
||||
.catch((error) => {
|
||||
that.props.onPublishSubmitError(error.message);
|
||||
this.props.onPublishSubmitError(error.message);
|
||||
});
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div className="row row--no-bottom">
|
||||
<div className="column column--10">
|
||||
<div className='row row--no-bottom'>
|
||||
<div className='column column--10'>
|
||||
<PublishTitleInput />
|
||||
</div>
|
||||
{/* left column */}
|
||||
<div className="column column--5 column--sml-10" >
|
||||
<div className="row row--padded">
|
||||
<div className='column column--5 column--sml-10' >
|
||||
<div className='row row--padded'>
|
||||
<Dropzone />
|
||||
</div>
|
||||
</div>
|
||||
{/* right column */}
|
||||
<div className="column column--5 column--sml-10 align-content-top">
|
||||
<div id="publish-active-area" className="row row--padded">
|
||||
<div className="row row--padded row--no-top row--wide">
|
||||
<div className='column column--5 column--sml-10 align-content-top'>
|
||||
<div id='publish-active-area' className='row row--padded'>
|
||||
<div className='row row--padded row--no-top row--wide'>
|
||||
<PublishUrlInput />
|
||||
</div>
|
||||
<div className="row row--padded row--no-top row--wide">
|
||||
<div className='row row--padded row--no-top row--wide'>
|
||||
<ChannelSelect />
|
||||
</div>
|
||||
{ (this.props.file.type === 'video/mp4') && (
|
||||
<div className="row row--padded row--no-top row--wide ">
|
||||
<div className='row row--padded row--no-top row--wide '>
|
||||
<PublishThumbnailInput />
|
||||
</div>
|
||||
)}
|
||||
<div className="row row--padded row--no-top row--no-bottom row--wide">
|
||||
<div className='row row--padded row--no-top row--no-bottom row--wide'>
|
||||
<PublishMetadataInputs />
|
||||
</div>
|
||||
<div className="row row--wide align-content-center">
|
||||
<button id="publish-submit" className="button--primary button--large" onClick={this.publish}>Publish</button>
|
||||
<div className='row row--wide align-content-center'>
|
||||
<button id='publish-submit' className='button--primary button--large' onClick={this.publish}>Publish</button>
|
||||
</div>
|
||||
<div className="row row--padded row--no-bottom align-content-center">
|
||||
<button className="button--cancel" onClick={this.props.onFileClear}>Cancel</button>
|
||||
<div className='row row--padded row--no-bottom align-content-center'>
|
||||
<button className='button--cancel' onClick={this.props.onFileClear}>Cancel</button>
|
||||
</div>
|
||||
<div className="row row--short align-content-center">
|
||||
<p className="fine-print">By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className="link--primary" target="_blank" href="https://lbry.io/learn">Read more.</a></p>
|
||||
<div className='row row--short align-content-center'>
|
||||
<p className='fine-print'>By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className='link--primary' target='_blank' href='https://lbry.io/learn'>Read more.</a></p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
|
|
@ -65,7 +65,7 @@ class PublishMetadataInputs extends React.Component {
|
|||
</div>
|
||||
</div>
|
||||
)}
|
||||
<a className="label link--primary" id="publish-details-toggle" href="#" onClick={this.toggleShowInputs}>{this.props.showMetadataInputs ? '[less]' : '[more]'}</a>
|
||||
<button className="button--secondary" onClick={this.toggleShowInputs}>{this.props.showMetadataInputs ? 'less' : 'more'}</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,8 +17,7 @@ const ThumbnailPreview = ({dataUrl}) => {
|
|||
<img style={imageStyle} src={dataUrl} alt='image preview here' />
|
||||
) : (
|
||||
<p>loading...</p>
|
||||
)
|
||||
}
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -27,8 +26,12 @@ class PublishThumbnailInput extends React.Component {
|
|||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
videoPreviewSrc: null,
|
||||
thumbnailError : null,
|
||||
thumbnailInput : '',
|
||||
}
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.updateVideoThumb = this.updateVideoThumb.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.setClaimAndThumbailUrl(this.props.publishClaim);
|
||||
|
|
|
@ -14,6 +14,6 @@ const mapDispatchToProps = dispatch => {
|
|||
dispatch(updateMetadata(name, value));
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -6,9 +6,6 @@ class PublishUrlInput extends React.Component {
|
|||
constructor (props) {
|
||||
super(props);
|
||||
this.handleInput = this.handleInput.bind(this);
|
||||
this.cleanseInput = this.cleanseInput.bind(this);
|
||||
this.setClaimNameFromFileName = this.setClaimNameFromFileName.bind(this);
|
||||
this.checkClaimIsAvailable = this.checkClaimIsAvailable.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
if (!this.props.claim || this.props.claim === '') {
|
||||
|
@ -40,18 +37,17 @@ class PublishUrlInput extends React.Component {
|
|||
this.props.onClaimChange(cleanClaimName);
|
||||
}
|
||||
checkClaimIsAvailable (claim) {
|
||||
const that = this;
|
||||
request(`/api/claim-is-available/${claim}`)
|
||||
request(`/api/claim/availability/${claim}`)
|
||||
.then(isAvailable => {
|
||||
// console.log('checkClaimIsAvailable request response:', isAvailable);
|
||||
if (isAvailable) {
|
||||
that.props.onUrlError(null);
|
||||
this.props.onUrlError(null);
|
||||
} else {
|
||||
that.props.onUrlError('That url has already been claimed');
|
||||
this.props.onUrlError('That url has already been claimed');
|
||||
}
|
||||
})
|
||||
.catch((error) => {
|
||||
that.props.onUrlError(error.message);
|
||||
this.props.onUrlError(error.message);
|
||||
});
|
||||
}
|
||||
render () {
|
||||
|
|
|
@ -1,26 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import View from './view';
|
||||
import { updateAssetClaimData } from 'actions/show';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
return {
|
||||
modifier : show.assetRequest.modifier,
|
||||
claim : show.assetRequest.name,
|
||||
extension: show.assetRequest.extension,
|
||||
claimData: show.showAsset.claimData.data,
|
||||
shortId : show.showAsset.claimData.shortId,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onAssetClaimDataUpdate: (claimData, shortId) => {
|
||||
dispatch(updateAssetClaimData(claimData, shortId));
|
||||
},
|
||||
onAssetClaimDataClear: () => {
|
||||
dispatch(updateAssetClaimData(null, null));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
|
@ -1,129 +0,0 @@
|
|||
import React from 'react';
|
||||
import ShowAssetLite from 'components/ShowAssetLite';
|
||||
import ShowAssetDetails from 'components/ShowAssetDetails';
|
||||
import request from 'utils/request';
|
||||
|
||||
class ShowAsset extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
this.getLongClaimId = this.getLongClaimId.bind(this);
|
||||
this.getClaimData = this.getClaimData.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
console.log('ShowAsset did mount');
|
||||
console.log('ShowAsset props', this.props);
|
||||
const modifier = this.props.modifier;
|
||||
const name = this.props.claim;
|
||||
// create request params
|
||||
let body = {};
|
||||
if (modifier) {
|
||||
if (modifier.id) {
|
||||
body['claimId'] = modifier.id;
|
||||
} else {
|
||||
body['channelName'] = modifier.channel.name;
|
||||
body['channelClaimId'] = modifier.channel.id;
|
||||
}
|
||||
}
|
||||
body['claimName'] = name;
|
||||
const params = {
|
||||
method : 'POST',
|
||||
headers: new Headers({
|
||||
'Content-Type': 'application/json',
|
||||
}),
|
||||
body: JSON.stringify(body),
|
||||
}
|
||||
// make request
|
||||
const that = this;
|
||||
this.getLongClaimId(params)
|
||||
.then(claimLongId => {
|
||||
return Promise.all([that.getShortClaimId(claimLongId, name), that.getClaimData(claimLongId, name)]);
|
||||
})
|
||||
.then(([shortId, claimData]) => {
|
||||
this.setState({error: null}); // note: move this to redux level
|
||||
this.props.onAssetClaimDataUpdate(claimData, shortId);
|
||||
})
|
||||
.catch(error => {
|
||||
this.setState({error});
|
||||
});
|
||||
}
|
||||
getLongClaimId (params) {
|
||||
const url = `/api/claim-get-long-id`;
|
||||
console.log('params:', params);
|
||||
return new Promise((resolve, reject) => {
|
||||
request(url, params)
|
||||
.then(({ success, message, data }) => {
|
||||
console.log('get long claim id response:', message);
|
||||
if (!success) {
|
||||
reject(message);
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
getShortClaimId (longId, name) {
|
||||
const url = `/api/claim-shorten-id/${longId}/${name}`;
|
||||
return new Promise((resolve, reject) => {
|
||||
request(url)
|
||||
.then(({ success, message, data }) => {
|
||||
console.log('get short claim id response:', data);
|
||||
if (!success) {
|
||||
reject(message);
|
||||
}
|
||||
resolve(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
getClaimData (claimId, claimName) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const url = `/api/claim-get-data/${claimName}/${claimId}`;
|
||||
return request(url)
|
||||
.then(({ success, message }) => {
|
||||
console.log('get claim data response:', message);
|
||||
if (!success) {
|
||||
reject(message);
|
||||
}
|
||||
resolve(message);
|
||||
})
|
||||
.catch((error) => {
|
||||
reject(error.message);
|
||||
});
|
||||
});
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.onAssetClaimDataClear();
|
||||
}
|
||||
render () {
|
||||
if (this.props.claimData) {
|
||||
if (this.props.extension) {
|
||||
return (
|
||||
<ShowAssetLite
|
||||
error={this.state.error}
|
||||
claimData={this.props.claimData}
|
||||
/>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<ShowAssetDetails
|
||||
error={this.state.error}
|
||||
claimData={this.props.claimData}
|
||||
shortId={this.props.shortId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
};
|
||||
return (
|
||||
<div></div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowAsset;
|
|
@ -1,26 +0,0 @@
|
|||
import { connect } from 'react-redux';
|
||||
import {updateChannelData} from 'actions/show';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
return {
|
||||
requestName: show.channelRequest.name,
|
||||
requestId : show.channelRequest.id,
|
||||
name : show.showChannel.channelData.name,
|
||||
shortId : show.showChannel.channelData.shortId,
|
||||
longId : show.showChannel.channelData.longId,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChannelDataUpdate: (name, longId, shortId) => {
|
||||
dispatch(updateChannelData(name, longId, shortId));
|
||||
},
|
||||
onChannelDataClear: () => {
|
||||
dispatch(updateChannelData(null, null, null));
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
|
@ -1,69 +0,0 @@
|
|||
import React from 'react';
|
||||
import NavBar from 'containers/NavBar';
|
||||
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
|
||||
import request from 'utils/request';
|
||||
|
||||
class ShowChannel extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
this.getAndStoreChannelData = this.getAndStoreChannelData.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
this.getAndStoreChannelData(this.props.requestName, this.props.requestId);
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.requestName !== this.props.requestName || nextProps.requestId !== this.props.requestId) {
|
||||
this.getAndStoreChannelData(nextProps.requestName, nextProps.requestId);
|
||||
}
|
||||
}
|
||||
getAndStoreChannelData (name, id) {
|
||||
if (!id) id = 'none';
|
||||
const url = `/api/channel-data/${name}/${id}`;
|
||||
const that = this;
|
||||
return request(url)
|
||||
.then(({ success, message, data }) => {
|
||||
console.log('api/channel-data response:', data);
|
||||
if (!success) {
|
||||
return that.setState({error: message});
|
||||
}
|
||||
that.setState({error: null}); // note: store this error at app level also
|
||||
that.props.onChannelDataUpdate(data.channelName, data.longChannelClaimId, data.shortChannelClaimId);
|
||||
})
|
||||
.catch((error) => {
|
||||
that.setState({error: error.message});
|
||||
});
|
||||
}
|
||||
componentWillUnmount () {
|
||||
this.props.onChannelDataClear();
|
||||
}
|
||||
render () {
|
||||
return (
|
||||
<div>
|
||||
<NavBar/>
|
||||
{this.state.error ? (
|
||||
<div className="row row--tall row--padded">
|
||||
<div className="column column--10">
|
||||
<p>{this.state.error}</p>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<div className="row row--tall row--padded">
|
||||
<div className="column column--10">
|
||||
<h2>channel name: {this.props.name ? this.props.name : 'loading...'}</h2>
|
||||
<p>full channel id: {this.props.longId ? this.props.longId : 'loading...'}</p>
|
||||
<p>short channel id: {this.props.shortId ? this.props.shortId : 'loading...'}</p>
|
||||
</div>
|
||||
<div className="column column--10">
|
||||
{(this.props.name && this.props.longId) && <ChannelClaimsDisplay />}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default ShowChannel;
|
|
@ -1,22 +1,16 @@
|
|||
import { connect } from 'react-redux';
|
||||
import { updateRequestWithChannelRequest, updateRequestWithAssetRequest } from 'actions/show';
|
||||
import { onHandleShowPageUri } from 'actions/show';
|
||||
import View from './view';
|
||||
|
||||
const mapStateToProps = ({ show }) => {
|
||||
return {
|
||||
requestType: show.requestType,
|
||||
error : show.request.error,
|
||||
requestType: show.request.type,
|
||||
};
|
||||
};
|
||||
|
||||
const mapDispatchToProps = dispatch => {
|
||||
return {
|
||||
onChannelRequest: (name, id) => {
|
||||
dispatch(updateRequestWithChannelRequest(name, id));
|
||||
},
|
||||
onAssetRequest: (name, id, channelName, channelId, extension) => {
|
||||
dispatch(updateRequestWithAssetRequest(name, id, channelName, channelId, extension));
|
||||
},
|
||||
};
|
||||
const mapDispatchToProps = {
|
||||
onHandleShowPageUri,
|
||||
};
|
||||
|
||||
export default connect(mapStateToProps, mapDispatchToProps)(View);
|
||||
|
|
|
@ -1,94 +1,34 @@
|
|||
import React from 'react';
|
||||
import ErrorPage from 'components/ErrorPage';
|
||||
import ShowAsset from 'containers/ShowAsset';
|
||||
import ShowChannel from 'containers/ShowChannel';
|
||||
import lbryUri from 'utils/lbryUri';
|
||||
import ShowAssetLite from 'components/ShowAssetLite';
|
||||
import ShowAssetDetails from 'components/ShowAssetDetails';
|
||||
import ShowChannel from 'components/ShowChannel';
|
||||
|
||||
import { CHANNEL, ASSET } from 'constants/show_request_types';
|
||||
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
|
||||
|
||||
class ShowPage extends React.Component {
|
||||
constructor (props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
error: null,
|
||||
};
|
||||
this.parseUrlAndUpdateState = this.parseUrlAndUpdateState.bind(this);
|
||||
this.parseAndUpdateIdentifierAndClaim = this.parseAndUpdateIdentifierAndClaim.bind(this);
|
||||
this.parseAndUpdateClaimOnly = this.parseAndUpdateClaimOnly.bind(this);
|
||||
}
|
||||
componentDidMount () {
|
||||
console.log('ShowPage did mount');
|
||||
const identifier = this.props.match.params.identifier;
|
||||
const claim = this.props.match.params.claim;
|
||||
this.parseUrlAndUpdateState(identifier, claim);
|
||||
this.props.onHandleShowPageUri(this.props.match.params);
|
||||
}
|
||||
componentWillReceiveProps (nextProps) {
|
||||
if (nextProps.match.params !== this.props.match.params) {
|
||||
console.log('ShowPage received new params props');
|
||||
const identifier = nextProps.match.params.identifier;
|
||||
const claim = nextProps.match.params.claim;
|
||||
this.parseUrlAndUpdateState(identifier, claim);
|
||||
this.props.onHandleShowPageUri(nextProps.match.params);
|
||||
}
|
||||
}
|
||||
parseUrlAndUpdateState (identifier, claim) {
|
||||
if (identifier) {
|
||||
return this.parseAndUpdateIdentifierAndClaim(identifier, claim);
|
||||
}
|
||||
this.parseAndUpdateClaimOnly(claim);
|
||||
}
|
||||
parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||
// this is a request for an asset
|
||||
// claim will be an asset claim
|
||||
// the identifier could be a channel or a claim id
|
||||
let isChannel, channelName, channelClaimId, claimId, claimName, extension;
|
||||
try {
|
||||
({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(modifier));
|
||||
({ claimName, extension } = lbryUri.parseClaim(claim));
|
||||
} catch (error) {
|
||||
return this.setState({error: error.message});
|
||||
}
|
||||
// update the store
|
||||
if (isChannel) {
|
||||
return this.props.onAssetRequest(claimName, null, channelName, channelClaimId, extension);
|
||||
} else {
|
||||
return this.props.onAssetRequest(claimName, claimId, null, null, extension);
|
||||
}
|
||||
}
|
||||
parseAndUpdateClaimOnly (claim) {
|
||||
// this could be a request for an asset or a channel page
|
||||
// claim could be an asset claim or a channel claim
|
||||
let isChannel, channelName, channelClaimId;
|
||||
try {
|
||||
({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(claim));
|
||||
} catch (error) {
|
||||
return this.setState({error: error.message});
|
||||
}
|
||||
// return early if this request is for a channel
|
||||
if (isChannel) {
|
||||
return this.props.onChannelRequest(channelName, channelClaimId);
|
||||
}
|
||||
// if not for a channel, parse the claim request
|
||||
let claimName, extension; // if I am destructuring below, do I still need to declare these here?
|
||||
try {
|
||||
({claimName, extension} = lbryUri.parseClaim(claim));
|
||||
} catch (error) {
|
||||
return this.setState({error: error.message});
|
||||
}
|
||||
this.props.onAssetRequest(claimName, null, null, null, extension);
|
||||
}
|
||||
render () {
|
||||
console.log('rendering ShowPage');
|
||||
console.log('ShowPage props', this.props);
|
||||
if (this.state.error) {
|
||||
const { error, requestType } = this.props;
|
||||
if (error) {
|
||||
return (
|
||||
<ErrorPage error={this.state.error}/>
|
||||
<ErrorPage error={error} />
|
||||
);
|
||||
}
|
||||
switch (this.props.requestType) {
|
||||
switch (requestType) {
|
||||
case CHANNEL:
|
||||
return <ShowChannel />;
|
||||
case ASSET:
|
||||
return <ShowAsset />;
|
||||
case ASSET_LITE:
|
||||
return <ShowAssetLite />;
|
||||
case ASSET_DETAILS:
|
||||
return <ShowAssetDetails />;
|
||||
default:
|
||||
return <p>loading...</p>;
|
||||
}
|
||||
|
|
|
@ -1,15 +0,0 @@
|
|||
import React from 'react';
|
||||
import { render } from 'react-dom';
|
||||
import { createStore } from 'redux';
|
||||
import Reducer from 'reducers';
|
||||
import Root from './root';
|
||||
|
||||
let store = createStore(
|
||||
Reducer,
|
||||
window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
|
||||
);
|
||||
|
||||
render(
|
||||
<Root store={store} />,
|
||||
document.getElementById('react-app')
|
||||
);
|
|
@ -8,19 +8,11 @@ const initialState = {
|
|||
},
|
||||
};
|
||||
|
||||
/*
|
||||
Reducers describe how the application's state changes in response to actions
|
||||
*/
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.CHANNEL_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
loggedInChannel: {
|
||||
name : action.name,
|
||||
shortId: action.shortId,
|
||||
longId : action.longId,
|
||||
},
|
||||
loggedInChannel: action.data,
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -34,27 +34,23 @@ const initialState = {
|
|||
},
|
||||
};
|
||||
|
||||
/*
|
||||
Reducers describe how the application's state changes in response to actions
|
||||
*/
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.FILE_SELECTED:
|
||||
return Object.assign({}, state, {
|
||||
file: action.file,
|
||||
file: action.data,
|
||||
});
|
||||
case actions.FILE_CLEAR:
|
||||
return initialState;
|
||||
case actions.METADATA_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
metadata: Object.assign({}, state.metadata, {
|
||||
[action.name]: action.value,
|
||||
[action.data.name]: action.data.value,
|
||||
}),
|
||||
});
|
||||
case actions.CLAIM_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
claim: action.value,
|
||||
claim: action.data,
|
||||
});
|
||||
case actions.SET_PUBLISH_IN_CHANNEL:
|
||||
return Object.assign({}, state, {
|
||||
|
@ -62,24 +58,21 @@ export default function (state = initialState, action) {
|
|||
});
|
||||
case actions.PUBLISH_STATUS_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
status: Object.assign({}, state.status, {
|
||||
status : action.status,
|
||||
message: action.message,
|
||||
}),
|
||||
status: action.data,
|
||||
});
|
||||
case actions.ERROR_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
error: Object.assign({}, state.error, {
|
||||
[action.name]: action.value,
|
||||
[action.data.name]: action.data.value,
|
||||
}),
|
||||
});
|
||||
case actions.SELECTED_CHANNEL_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
selectedChannel: action.value,
|
||||
selectedChannel: action.data,
|
||||
});
|
||||
case actions.TOGGLE_METADATA_INPUTS:
|
||||
return Object.assign({}, state, {
|
||||
showMetadataInputs: action.value,
|
||||
showMetadataInputs: action.data,
|
||||
});
|
||||
case actions.THUMBNAIL_CLAIM_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
|
|
|
@ -1,102 +1,93 @@
|
|||
import * as actions from 'constants/show_action_types';
|
||||
import { CHANNEL, ASSET } from 'constants/show_request_types';
|
||||
import { LOCAL_CHECK, ERROR } from 'constants/asset_display_states';
|
||||
|
||||
const initialState = {
|
||||
requestType : null,
|
||||
channelRequest: {
|
||||
name: null,
|
||||
id : null,
|
||||
request: {
|
||||
error: null,
|
||||
type : null,
|
||||
id : null,
|
||||
},
|
||||
assetRequest: {
|
||||
name : null,
|
||||
modifier: {
|
||||
id : null,
|
||||
channel: {
|
||||
name: null,
|
||||
id : null,
|
||||
},
|
||||
},
|
||||
extension: null,
|
||||
},
|
||||
showChannel: {
|
||||
channelData: {
|
||||
name : null,
|
||||
shortId: null,
|
||||
longId : null,
|
||||
},
|
||||
channelClaimsData: {
|
||||
claims : null,
|
||||
currentPage: null,
|
||||
totalPages : null,
|
||||
totalClaims: null,
|
||||
},
|
||||
},
|
||||
showAsset: {
|
||||
claimData: {
|
||||
data : null,
|
||||
shortId: null,
|
||||
},
|
||||
requestList : {},
|
||||
channelList : {},
|
||||
assetList : {},
|
||||
displayAsset: {
|
||||
error : null,
|
||||
status: LOCAL_CHECK,
|
||||
},
|
||||
};
|
||||
|
||||
/*
|
||||
Reducers describe how the application's state changes in response to actions
|
||||
*/
|
||||
|
||||
export default function (state = initialState, action) {
|
||||
switch (action.type) {
|
||||
case actions.REQUEST_UPDATE_CHANNEL:
|
||||
// handle request
|
||||
case actions.REQUEST_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
requestType : CHANNEL,
|
||||
channelRequest: {
|
||||
name: action.name,
|
||||
id : action.id,
|
||||
},
|
||||
request: Object.assign({}, state.request, {
|
||||
error: action.data,
|
||||
}),
|
||||
});
|
||||
case actions.REQUEST_UPDATE_CLAIM:
|
||||
case actions.REQUEST_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
requestType : ASSET,
|
||||
assetRequest: {
|
||||
name : action.name,
|
||||
modifier: {
|
||||
id : action.id,
|
||||
channel: {
|
||||
name: action.channelName,
|
||||
id : action.channelId,
|
||||
},
|
||||
request: Object.assign({}, state.request, {
|
||||
type: action.data.requestType,
|
||||
id : action.data.requestId,
|
||||
}),
|
||||
});
|
||||
// store requests
|
||||
case actions.REQUEST_LIST_ADD:
|
||||
return Object.assign({}, state, {
|
||||
requestList: Object.assign({}, state.requestList, {
|
||||
[action.data.id]: {
|
||||
error: action.data.error,
|
||||
key : action.data.key,
|
||||
},
|
||||
extension: action.extension,
|
||||
},
|
||||
}),
|
||||
});
|
||||
case actions.CHANNEL_DATA_UPDATE:
|
||||
// asset data
|
||||
case actions.ASSET_ADD:
|
||||
return Object.assign({}, state, {
|
||||
showChannel: Object.assign({}, state.showChannel, {
|
||||
channelData: Object.assign({}, state.channelData, {
|
||||
name : action.name,
|
||||
shortId: action.shortId,
|
||||
longId : action.longId,
|
||||
assetList: Object.assign({}, state.assetList, {
|
||||
[action.data.id]: {
|
||||
error : action.data.error,
|
||||
name : action.data.name,
|
||||
claimId : action.data.claimId,
|
||||
shortId : action.data.shortId,
|
||||
claimData: action.data.claimData,
|
||||
},
|
||||
}),
|
||||
});
|
||||
// channel data
|
||||
case actions.CHANNEL_ADD:
|
||||
return Object.assign({}, state, {
|
||||
channelList: Object.assign({}, state.channelList, {
|
||||
[action.data.id]: {
|
||||
name : action.data.name,
|
||||
longId : action.data.longId,
|
||||
shortId : action.data.shortId,
|
||||
claimsData: action.data.claimsData,
|
||||
},
|
||||
}),
|
||||
});
|
||||
case actions.CHANNEL_CLAIMS_UPDATE_SUCCESS:
|
||||
return Object.assign({}, state, {
|
||||
channelList: Object.assign({}, state.channelList, {
|
||||
[action.data.channelListId]: Object.assign({}, state.channelList[action.data.channelListId], {
|
||||
claimsData: action.data.claimsData,
|
||||
}),
|
||||
}),
|
||||
});
|
||||
case actions.CHANNEL_CLAIMS_DATA_UPDATE:
|
||||
// display an asset
|
||||
case actions.FILE_AVAILABILITY_UPDATE:
|
||||
return Object.assign({}, state, {
|
||||
showChannel: Object.assign({}, state.showChannel, {
|
||||
channelClaimsData: {
|
||||
claims : action.claims,
|
||||
currentPage: action.currentPage,
|
||||
totalPages : action.totalPages,
|
||||
totalClaims: action.totalClaims,
|
||||
},
|
||||
displayAsset: Object.assign({}, state.displayAsset, {
|
||||
status: action.data,
|
||||
}),
|
||||
});
|
||||
case actions.ASSET_CLAIM_DATA_UPDATE:
|
||||
case actions.DISPLAY_ASSET_ERROR:
|
||||
return Object.assign({}, state, {
|
||||
showAsset: {
|
||||
claimData: {
|
||||
data : action.data,
|
||||
shortId: action.shortId,
|
||||
},
|
||||
},
|
||||
displayAsset: Object.assign({}, state.displayAsset, {
|
||||
error : action.data,
|
||||
status: ERROR,
|
||||
}),
|
||||
});
|
||||
default:
|
||||
return state;
|
||||
|
|
|
@ -1,29 +0,0 @@
|
|||
import React from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { Provider } from 'react-redux';
|
||||
import { BrowserRouter, Route, Switch } from 'react-router-dom';
|
||||
|
||||
import PublishPage from 'components/PublishPage';
|
||||
import AboutPage from 'components/AboutPage';
|
||||
import LoginPage from 'containers/LoginPage';
|
||||
import ShowPage from 'containers/ShowPage';
|
||||
|
||||
const Root = ({ store }) => (
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<Switch>
|
||||
<Route exact path="/" component={PublishPage} />
|
||||
<Route exact path="/about" component={AboutPage} />
|
||||
<Route exact path="/login" component={LoginPage} />
|
||||
<Route exact path="/:identifier/:claim" component={ShowPage} />
|
||||
<Route exact path="/:claim/" component={ShowPage} />
|
||||
</Switch>
|
||||
</BrowserRouter>
|
||||
</Provider>
|
||||
);
|
||||
|
||||
Root.propTypes = {
|
||||
store: PropTypes.object.isRequired,
|
||||
};
|
||||
|
||||
export default Root;
|
33
react/sagas/file.js
Normal file
33
react/sagas/file.js
Normal file
|
@ -0,0 +1,33 @@
|
|||
import { call, put, takeLatest } from 'redux-saga/effects';
|
||||
import * as actions from 'constants/show_action_types';
|
||||
import { updateFileAvailability, updateDisplayAssetError } from 'actions/show';
|
||||
import { UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states';
|
||||
import { checkFileAvailability, triggerClaimGet } from 'api/fileApi';
|
||||
|
||||
function * retrieveFile (action) {
|
||||
const name = action.data.name;
|
||||
const claimId = action.data.claimId;
|
||||
// see if the file is available
|
||||
let isAvailable;
|
||||
try {
|
||||
({ data: isAvailable } = yield call(checkFileAvailability, name, claimId));
|
||||
} catch (error) {
|
||||
return yield put(updateDisplayAssetError(error.message));
|
||||
};
|
||||
if (isAvailable) {
|
||||
yield put(updateDisplayAssetError(null));
|
||||
return yield put(updateFileAvailability(AVAILABLE));
|
||||
}
|
||||
yield put(updateFileAvailability(UNAVAILABLE));
|
||||
// initiate get request for the file
|
||||
try {
|
||||
yield call(triggerClaimGet, name, claimId);
|
||||
} catch (error) {
|
||||
return yield put(updateDisplayAssetError(error.message));
|
||||
};
|
||||
yield put(updateFileAvailability(AVAILABLE));
|
||||
};
|
||||
|
||||
export function * watchFileIsRequested () {
|
||||
yield takeLatest(actions.FILE_REQUESTED, retrieveFile);
|
||||
};
|
15
react/sagas/index.js
Normal file
15
react/sagas/index.js
Normal file
|
@ -0,0 +1,15 @@
|
|||
import { all } from 'redux-saga/effects';
|
||||
import { watchHandleShowPageUri } from './show_uri';
|
||||
import { watchNewAssetRequest } from './show_asset';
|
||||
import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';
|
||||
import { watchFileIsRequested } from './file';
|
||||
|
||||
export default function * rootSaga () {
|
||||
yield all([
|
||||
watchHandleShowPageUri(),
|
||||
watchNewAssetRequest(),
|
||||
watchNewChannelRequest(),
|
||||
watchUpdateChannelClaims(),
|
||||
watchFileIsRequested(),
|
||||
]);
|
||||
}
|
59
react/sagas/show_asset.js
Normal file
59
react/sagas/show_asset.js
Normal file
|
@ -0,0 +1,59 @@
|
|||
import { call, put, select, takeLatest } from 'redux-saga/effects';
|
||||
import * as actions from 'constants/show_action_types';
|
||||
import { addRequestToRequestList, onRequestError, onRequestUpdate, addAssetToAssetList } from 'actions/show';
|
||||
import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi';
|
||||
import { selectShowState } from 'selectors/show';
|
||||
|
||||
export function * newAssetRequest (action) {
|
||||
const { requestType, requestId, name, modifier } = action.data;
|
||||
// put an action to update the request in redux
|
||||
yield put(onRequestUpdate(requestType, requestId));
|
||||
// is this an existing request?
|
||||
// If this uri is in the request list, it's already been fetched
|
||||
const state = yield select(selectShowState);
|
||||
if (state.requestList[requestId]) {
|
||||
console.log('that request already exists in the request list!');
|
||||
return null;
|
||||
}
|
||||
// get long id && add request to request list
|
||||
console.log(`getting asset long id ${name}`);
|
||||
let longId;
|
||||
try {
|
||||
({data: longId} = yield call(getLongClaimId, name, modifier));
|
||||
} catch (error) {
|
||||
console.log('error:', error);
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
const assetKey = `a#${name}#${longId}`;
|
||||
yield put(addRequestToRequestList(requestId, null, assetKey));
|
||||
// is this an existing asset?
|
||||
// If this asset is in the asset list, it's already been fetched
|
||||
if (state.assetList[assetKey]) {
|
||||
console.log('that asset already exists in the asset list!');
|
||||
return null;
|
||||
}
|
||||
// get short Id
|
||||
console.log(`getting asset short id ${name} ${longId}`);
|
||||
let shortId;
|
||||
try {
|
||||
({data: shortId} = yield call(getShortId, name, longId));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
// get asset claim data
|
||||
console.log(`getting asset claim data ${name} ${longId}`);
|
||||
let claimData;
|
||||
try {
|
||||
({data: claimData} = yield call(getClaimData, name, longId));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
// add asset to asset list
|
||||
yield put(addAssetToAssetList(assetKey, null, name, longId, shortId, claimData));
|
||||
// clear any errors in request error
|
||||
yield put(onRequestError(null));
|
||||
};
|
||||
|
||||
export function * watchNewAssetRequest () {
|
||||
yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest);
|
||||
};
|
66
react/sagas/show_channel.js
Normal file
66
react/sagas/show_channel.js
Normal file
|
@ -0,0 +1,66 @@
|
|||
import {call, put, select, takeLatest} from 'redux-saga/effects';
|
||||
import * as actions from 'constants/show_action_types';
|
||||
import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from 'actions/show';
|
||||
import { getChannelClaims, getChannelData } from 'api/channelApi';
|
||||
import { selectShowState } from 'selectors/show';
|
||||
|
||||
export function * newChannelRequest (action) {
|
||||
const { requestType, requestId, channelName, channelId } = action.data;
|
||||
// put an action to update the request in redux
|
||||
yield put(onRequestUpdate(requestType, requestId));
|
||||
// is this an existing request?
|
||||
// If this uri is in the request list, it's already been fetched
|
||||
const state = yield select(selectShowState);
|
||||
if (state.requestList[requestId]) {
|
||||
console.log('that request already exists in the request list!');
|
||||
return null;
|
||||
}
|
||||
// get channel long id
|
||||
console.log('getting channel long id and short id');
|
||||
let longId, shortId;
|
||||
try {
|
||||
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, channelName, channelId));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
// store the request in the channel requests list
|
||||
const channelKey = `c#${channelName}#${longId}`;
|
||||
yield put(addRequestToRequestList(requestId, null, channelKey));
|
||||
// is this an existing channel?
|
||||
// If this channel is in the channel list, it's already been fetched
|
||||
if (state.channelList[channelKey]) {
|
||||
console.log('that channel already exists in the channel list!');
|
||||
return null;
|
||||
}
|
||||
// get channel claims data
|
||||
console.log('getting channel claims data');
|
||||
let claimsData;
|
||||
try {
|
||||
({ data: claimsData } = yield call(getChannelClaims, channelName, longId, 1));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
// store the channel data in the channel list
|
||||
yield put(addNewChannelToChannelList(channelKey, channelName, shortId, longId, claimsData));
|
||||
// clear any request errors
|
||||
yield put(onRequestError(null));
|
||||
}
|
||||
|
||||
export function * watchNewChannelRequest () {
|
||||
yield takeLatest(actions.CHANNEL_REQUEST_NEW, newChannelRequest);
|
||||
};
|
||||
|
||||
function * getNewClaimsAndUpdateChannel (action) {
|
||||
const { channelKey, name, longId, page } = action.data;
|
||||
let claimsData;
|
||||
try {
|
||||
({ data: claimsData } = yield call(getChannelClaims, name, longId, page));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
yield put(updateChannelClaims(channelKey, claimsData));
|
||||
}
|
||||
|
||||
export function * watchUpdateChannelClaims () {
|
||||
yield takeLatest(actions.CHANNEL_CLAIMS_UPDATE_ASYNC, getNewClaimsAndUpdateChannel);
|
||||
}
|
62
react/sagas/show_uri.js
Normal file
62
react/sagas/show_uri.js
Normal file
|
@ -0,0 +1,62 @@
|
|||
import { call, put, takeLatest } from 'redux-saga/effects';
|
||||
import * as actions from 'constants/show_action_types';
|
||||
import { onRequestError, onNewChannelRequest, onNewAssetRequest } from 'actions/show';
|
||||
import { newAssetRequest } from 'sagas/show_asset';
|
||||
import { newChannelRequest } from 'sagas/show_channel';
|
||||
import lbryUri from 'utils/lbryUri';
|
||||
|
||||
function * parseAndUpdateIdentifierAndClaim (modifier, claim) {
|
||||
console.log('parseAndUpdateIdentifierAndClaim');
|
||||
// this is a request for an asset
|
||||
// claim will be an asset claim
|
||||
// the identifier could be a channel or a claim id
|
||||
let isChannel, channelName, channelClaimId, claimId, claimName, extension;
|
||||
try {
|
||||
({ isChannel, channelName, channelClaimId, claimId } = lbryUri.parseIdentifier(modifier));
|
||||
({ claimName, extension } = lbryUri.parseClaim(claim));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
// trigger an new action to update the store
|
||||
if (isChannel) {
|
||||
return yield call(newAssetRequest, onNewAssetRequest(claimName, null, channelName, channelClaimId, extension));
|
||||
};
|
||||
yield call(newAssetRequest, onNewAssetRequest(claimName, claimId, null, null, extension));
|
||||
}
|
||||
function * parseAndUpdateClaimOnly (claim) {
|
||||
console.log('parseAndUpdateIdentifierAndClaim');
|
||||
// this could be a request for an asset or a channel page
|
||||
// claim could be an asset claim or a channel claim
|
||||
let isChannel, channelName, channelClaimId;
|
||||
try {
|
||||
({ isChannel, channelName, channelClaimId } = lbryUri.parseIdentifier(claim));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
// trigger an new action to update the store
|
||||
// return early if this request is for a channel
|
||||
if (isChannel) {
|
||||
return yield call(newChannelRequest, onNewChannelRequest(channelName, channelClaimId));
|
||||
}
|
||||
// if not for a channel, parse the claim request
|
||||
let claimName, extension;
|
||||
try {
|
||||
({claimName, extension} = lbryUri.parseClaim(claim));
|
||||
} catch (error) {
|
||||
return yield put(onRequestError(error.message));
|
||||
}
|
||||
yield call(newAssetRequest, onNewAssetRequest(claimName, null, null, null, extension));
|
||||
}
|
||||
|
||||
export function * handleShowPageUri (action) {
|
||||
console.log('handleShowPageUri');
|
||||
const { identifier, claim } = action.data;
|
||||
if (identifier) {
|
||||
return yield call(parseAndUpdateIdentifierAndClaim, identifier, claim);
|
||||
}
|
||||
yield call(parseAndUpdateClaimOnly, claim);
|
||||
};
|
||||
|
||||
export function * watchHandleShowPageUri () {
|
||||
yield takeLatest(actions.HANDLE_SHOW_URI, handleShowPageUri);
|
||||
};
|
9
react/selectors/show.js
Normal file
9
react/selectors/show.js
Normal file
|
@ -0,0 +1,9 @@
|
|||
export const selectAsset = (show) => {
|
||||
const request = show.requestList[show.request.id];
|
||||
const assetKey = request.key;
|
||||
return show.assetList[assetKey];
|
||||
};
|
||||
|
||||
export const selectShowState = (state) => {
|
||||
return state.show;
|
||||
};
|
37
react/utils/canonicalLink.js
Normal file
37
react/utils/canonicalLink.js
Normal 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();
|
||||
};
|
|
@ -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.');
|
||||
}
|
||||
},
|
||||
}
|
||||
};
|
||||
|
|
|
@ -4,15 +4,13 @@ module.exports = {
|
|||
REGEXP_ADDRESS : /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/,
|
||||
CHANNEL_CHAR : '@',
|
||||
parseIdentifier : function (identifier) {
|
||||
console.log('parsing identifier:', identifier);
|
||||
const componentsRegex = new RegExp(
|
||||
'([^:$#/]*)' + // value (stops at the first separator or end)
|
||||
'([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)
|
||||
);
|
||||
const [proto, value, modifierSeperator, modifier] = componentsRegex
|
||||
const [proto, value, modifierSeperator, modifier] = componentsRegex // eslint-disable-line no-unused-vars
|
||||
.exec(identifier)
|
||||
.map(match => match || null);
|
||||
console.log(`${proto}, ${value}, ${modifierSeperator}, ${modifier}`);
|
||||
|
||||
// Validate and process name
|
||||
if (!value) {
|
||||
|
@ -54,15 +52,13 @@ module.exports = {
|
|||
};
|
||||
},
|
||||
parseClaim: function (name) {
|
||||
console.log('parsing name:', name);
|
||||
const componentsRegex = new RegExp(
|
||||
'([^:$#/.]*)' + // name (stops at the first extension)
|
||||
'([:$#.]?)([^/]*)' // extension separator, extension (stops at the first path separator or end)
|
||||
);
|
||||
const [proto, claimName, extensionSeperator, extension] = componentsRegex
|
||||
const [proto, claimName, extensionSeperator, extension] = componentsRegex // eslint-disable-line no-unused-vars
|
||||
.exec(name)
|
||||
.map(match => match || null);
|
||||
console.log(`${proto}, ${claimName}, ${extensionSeperator}, ${extension}`);
|
||||
|
||||
// Validate and process name
|
||||
if (!claimName) {
|
||||
|
|
96
react/utils/metaTags.js
Normal file
96
react/utils/metaTags.js
Normal 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
8
react/utils/pageTitle.js
Normal file
|
@ -0,0 +1,8 @@
|
|||
const { site: { title: siteTitle } } = require('../../config/speechConfig.js');
|
||||
|
||||
export const createPageTitle = (pageTitle) => {
|
||||
if (!pageTitle) {
|
||||
return `${siteTitle}`;
|
||||
}
|
||||
return `${siteTitle} - ${pageTitle}`;
|
||||
};
|
|
@ -1,3 +1,5 @@
|
|||
import 'cross-fetch/polyfill';
|
||||
|
||||
/**
|
||||
* Parses the JSON returned by a network request
|
||||
*
|
||||
|
@ -13,18 +15,18 @@ function parseJSON (response) {
|
|||
}
|
||||
|
||||
/**
|
||||
* Checks if a network request came back fine, and throws an error if not
|
||||
* Parses the status returned by a network request
|
||||
*
|
||||
* @param {object} response A response from a network request
|
||||
* @param {object} response The parsed JSON from the network request
|
||||
*
|
||||
* @return {object|undefined} Returns either the response, or throws an error
|
||||
* @return {object | undefined} Returns object with status and statusText, or undefined
|
||||
*/
|
||||
function checkStatus (response) {
|
||||
function checkStatus (response, jsonResponse) {
|
||||
if (response.status >= 200 && response.status < 300) {
|
||||
return response;
|
||||
return jsonResponse;
|
||||
}
|
||||
|
||||
const error = new Error(response.statusText);
|
||||
const error = new Error(jsonResponse.message);
|
||||
error.response = response;
|
||||
throw error;
|
||||
}
|
||||
|
@ -37,8 +39,13 @@ function checkStatus (response) {
|
|||
*
|
||||
* @return {object} The response data
|
||||
*/
|
||||
|
||||
export default function request (url, options) {
|
||||
return fetch(url, options)
|
||||
.then(checkStatus)
|
||||
.then(parseJSON);
|
||||
.then(response => {
|
||||
return Promise.all([response, parseJSON(response)]);
|
||||
})
|
||||
.then(([response, jsonResponse]) => {
|
||||
return checkStatus(response, jsonResponse);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -5,9 +5,9 @@ const multipartMiddleware = multipart({uploadDir: files.uploadDirectory});
|
|||
const db = require('../models');
|
||||
const { checkClaimNameAvailability, checkChannelAvailability, publish } = require('../controllers/publishController.js');
|
||||
const { getClaimList, resolveUri, getClaim } = require('../helpers/lbryApi.js');
|
||||
const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel, addGetResultsToFileData, createFileData, returnPublishTimingActionType } = require('../helpers/publishHelpers.js');
|
||||
const { createPublishParams, parsePublishApiRequestBody, parsePublishApiRequestFiles, parsePublishApiChannel, addGetResultsToFileData, createFileData } = require('../helpers/publishHelpers.js');
|
||||
const errorHandlers = require('../helpers/errorHandlers.js');
|
||||
const { sendGoogleAnalyticsTiming } = require('../helpers/statsHelpers.js');
|
||||
const { sendGAAnonymousPublishTiming, sendGAChannelPublishTiming } = require('../helpers/googleAnalytics.js');
|
||||
const { authenticateIfNoUserToken } = require('../auth/authentication.js');
|
||||
const { getChannelData, getChannelClaims, getClaimId } = require('../controllers/serveController.js');
|
||||
|
||||
|
@ -15,34 +15,73 @@ const NO_CHANNEL = 'NO_CHANNEL';
|
|||
const NO_CLAIM = 'NO_CLAIM';
|
||||
|
||||
module.exports = (app) => {
|
||||
// route to run a claim_list request on the daemon
|
||||
app.get('/api/claim-list/:name', ({ ip, originalUrl, params }, res) => {
|
||||
getClaimList(params.name)
|
||||
.then(claimsList => {
|
||||
res.status(200).json(claimsList);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to see if asset is available locally
|
||||
app.get('/api/file-is-available/:name/:claimId', ({ ip, originalUrl, params }, res) => {
|
||||
const name = params.name;
|
||||
const claimId = params.claimId;
|
||||
let isAvailable = false;
|
||||
db.File.findOne({where: {name, claimId}})
|
||||
// route to check whether site has published to a channel
|
||||
app.get('/api/channel/availability/:name', ({ ip, originalUrl, params }, res) => {
|
||||
checkChannelAvailability(params.name)
|
||||
.then(result => {
|
||||
if (result) {
|
||||
isAvailable = true;
|
||||
if (result === true) {
|
||||
res.status(200).json(true);
|
||||
} else {
|
||||
res.status(200).json(false);
|
||||
}
|
||||
res.status(200).json({success: true, data: isAvailable});
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to get a short channel id from long channel Id
|
||||
app.get('/api/channel/short-id/:longId/:name', ({ ip, originalUrl, params }, res) => {
|
||||
db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name)
|
||||
.then(shortId => {
|
||||
res.status(200).json(shortId);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
app.get('/api/channel/data/:channelName/:channelClaimId', ({ ip, originalUrl, body, params }, res) => {
|
||||
const channelName = params.channelName;
|
||||
let channelClaimId = params.channelClaimId;
|
||||
if (channelClaimId === 'none') channelClaimId = null;
|
||||
getChannelData(channelName, channelClaimId, 0)
|
||||
.then(data => {
|
||||
if (data === NO_CHANNEL) {
|
||||
return res.status(404).json({success: false, message: 'No matching channel was found'});
|
||||
}
|
||||
res.status(200).json({success: true, data});
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
app.get('/api/channel/claims/:channelName/:channelClaimId/:page', ({ ip, originalUrl, body, params }, res) => {
|
||||
const channelName = params.channelName;
|
||||
let channelClaimId = params.channelClaimId;
|
||||
if (channelClaimId === 'none') channelClaimId = null;
|
||||
const page = params.page;
|
||||
getChannelClaims(channelName, channelClaimId, page)
|
||||
.then(data => {
|
||||
if (data === NO_CHANNEL) {
|
||||
return res.status(404).json({success: false, message: 'No matching channel was found'});
|
||||
}
|
||||
res.status(200).json({success: true, data});
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to run a claim_list request on the daemon
|
||||
app.get('/api/claim/list/:name', ({ ip, originalUrl, params }, res) => {
|
||||
getClaimList(params.name)
|
||||
.then(claimsList => {
|
||||
res.status(200).json(claimsList);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to get an asset
|
||||
app.get('/api/claim-get/:name/:claimId', ({ ip, originalUrl, params }, res) => {
|
||||
app.get('/api/claim/get/:name/:claimId', ({ ip, originalUrl, params }, res) => {
|
||||
const name = params.name;
|
||||
const claimId = params.claimId;
|
||||
// resolve the claim
|
||||
|
@ -64,27 +103,12 @@ module.exports = (app) => {
|
|||
res.status(200).json({ success: true, message, completed });
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
|
||||
// route to check whether this site published to a claim
|
||||
app.get('/api/claim-is-available/:name', ({ params }, res) => {
|
||||
app.get('/api/claim/availability/:name', ({ ip, originalUrl, params }, res) => {
|
||||
checkClaimNameAvailability(params.name)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
res.status(200).json(true);
|
||||
} else {
|
||||
res.status(200).json(false);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
res.status(500).json(error);
|
||||
});
|
||||
});
|
||||
// route to check whether site has published to a channel
|
||||
app.get('/api/channel-is-available/:name', ({ params }, res) => {
|
||||
checkChannelAvailability(params.name)
|
||||
.then(result => {
|
||||
if (result === true) {
|
||||
res.status(200).json(true);
|
||||
|
@ -93,29 +117,27 @@ module.exports = (app) => {
|
|||
}
|
||||
})
|
||||
.catch(error => {
|
||||
res.status(500).json(error);
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to run a resolve request on the daemon
|
||||
app.get('/api/claim-resolve/:name/:claimId', ({ headers, ip, originalUrl, params }, res) => {
|
||||
app.get('/api/claim/resolve/:name/:claimId', ({ headers, ip, originalUrl, params }, res) => {
|
||||
resolveUri(`${params.name}#${params.claimId}`)
|
||||
.then(resolvedUri => {
|
||||
res.status(200).json(resolvedUri);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
.then(resolvedUri => {
|
||||
res.status(200).json(resolvedUri);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to run a publish request on the daemon
|
||||
app.post('/api/claim-publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => {
|
||||
app.post('/api/claim/publish', multipartMiddleware, ({ body, files, headers, ip, originalUrl, user }, res) => {
|
||||
logger.debug('api/claim-publish body:', body);
|
||||
logger.debug('api/claim-publish files:', files);
|
||||
// record the start time of the request and create variable for storing the action type
|
||||
const publishStartTime = Date.now();
|
||||
logger.debug('publish request started @', publishStartTime);
|
||||
let timingActionType;
|
||||
// define variables
|
||||
let name, fileName, filePath, fileType, nsfw, license, title, description, thumbnail, channelName, channelPassword;
|
||||
// record the start time of the request
|
||||
const publishStartTime = Date.now();
|
||||
// validate the body and files of the request
|
||||
try {
|
||||
// validateApiPublishRequest(body, files);
|
||||
|
@ -123,108 +145,62 @@ module.exports = (app) => {
|
|||
({fileName, filePath, fileType} = parsePublishApiRequestFiles(files));
|
||||
({channelName, channelPassword} = parsePublishApiChannel(body, user));
|
||||
} catch (error) {
|
||||
logger.debug('publish request rejected, insufficient request parameters', error);
|
||||
return res.status(400).json({success: false, message: error.message});
|
||||
}
|
||||
// check channel authorization
|
||||
authenticateIfNoUserToken(channelName, channelPassword, user)
|
||||
.then(authenticated => {
|
||||
if (!authenticated) {
|
||||
throw new Error('Authentication failed, you do not have access to that channel');
|
||||
}
|
||||
// make sure the claim name is available
|
||||
return checkClaimNameAvailability(name);
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
throw new Error('That name is already claimed by another user.');
|
||||
}
|
||||
// create publish parameters object
|
||||
return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName);
|
||||
})
|
||||
.then(publishParams => {
|
||||
// set the timing event type for reporting
|
||||
timingActionType = returnPublishTimingActionType(publishParams.channel_name);
|
||||
// publish the asset
|
||||
return publish(publishParams, fileName, fileType);
|
||||
})
|
||||
.then(result => {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'publish completed successfully',
|
||||
data : {
|
||||
name,
|
||||
claimId: result.claim_id,
|
||||
url : `${site.host}/${result.claim_id}/${name}`,
|
||||
lbryTx : result,
|
||||
},
|
||||
.then(authenticated => {
|
||||
if (!authenticated) {
|
||||
throw new Error('Authentication failed, you do not have access to that channel');
|
||||
}
|
||||
// make sure the claim name is available
|
||||
return checkClaimNameAvailability(name);
|
||||
})
|
||||
.then(result => {
|
||||
if (!result) {
|
||||
throw new Error('That name is already claimed by another user.');
|
||||
}
|
||||
// create publish parameters object
|
||||
return createPublishParams(filePath, name, title, description, license, nsfw, thumbnail, channelName);
|
||||
})
|
||||
.then(publishParams => {
|
||||
// publish the asset
|
||||
return publish(publishParams, fileName, fileType);
|
||||
})
|
||||
.then(result => {
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: 'publish completed successfully',
|
||||
data : {
|
||||
name,
|
||||
claimId: result.claim_id,
|
||||
url : `${site.host}/${result.claim_id}/${name}`,
|
||||
lbryTx : result,
|
||||
},
|
||||
});
|
||||
// record the publish end time and send to google analytics
|
||||
const publishEndTime = Date.now();
|
||||
if (channelName) {
|
||||
sendGAChannelPublishTiming(headers, ip, originalUrl, publishStartTime, publishEndTime);
|
||||
} else {
|
||||
sendGAAnonymousPublishTiming(headers, ip, originalUrl, publishStartTime, publishEndTime);
|
||||
}
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
// log the publish end time
|
||||
const publishEndTime = Date.now();
|
||||
logger.debug('publish request completed @', publishEndTime);
|
||||
sendGoogleAnalyticsTiming(timingActionType, headers, ip, originalUrl, publishStartTime, publishEndTime);
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to get a short claim id from long claim Id
|
||||
app.get('/api/claim-shorten-id/:longId/:name', ({ params }, res) => {
|
||||
app.get('/api/claim/short-id/:longId/:name', ({ ip, originalUrl, body, params }, res) => {
|
||||
db.Claim.getShortClaimIdFromLongClaimId(params.longId, params.name)
|
||||
.then(shortId => {
|
||||
res.status(200).json({success: true, data: shortId});
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('api error getting short channel id', error);
|
||||
res.status(200).json({success: false, message: error.message});
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to get a short channel id from long channel Id
|
||||
app.get('/api/channel-shorten-id/:longId/:name', ({ ip, originalUrl, params }, res) => {
|
||||
db.Certificate.getShortChannelIdFromLongChannelId(params.longId, params.name)
|
||||
.then(shortId => {
|
||||
logger.debug('sending back short channel id', shortId);
|
||||
res.status(200).json(shortId);
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('api error getting short channel id', error);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
app.get('/api/channel-data/:channelName/:channelClaimId', ({ ip, originalUrl, body, params }, res) => {
|
||||
const channelName = params.channelName;
|
||||
let channelClaimId = params.channelClaimId;
|
||||
if (channelClaimId === 'none') channelClaimId = null;
|
||||
getChannelData(channelName, channelClaimId, 0) // getChannelViewData(channelName, channelId, 0)
|
||||
.then(data => {
|
||||
if (data === NO_CHANNEL) {
|
||||
return res.status(200).json({success: false, message: 'No matching channel was found'});
|
||||
}
|
||||
res.status(200).json({success: true, data});
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('api error getting channel contents', error);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
app.get('/api/channel-claims/:channelName/:channelClaimId/:page', ({ ip, originalUrl, body, params }, res) => {
|
||||
const channelName = params.channelName;
|
||||
let channelClaimId = params.channelClaimId;
|
||||
if (channelClaimId === 'none') channelClaimId = null;
|
||||
const page = params.page;
|
||||
getChannelClaims(channelName, channelClaimId, page)// getChannelViewData(channelName, channelClaimId, page)
|
||||
.then(data => {
|
||||
if (data === NO_CHANNEL) {
|
||||
return res.status(200).json({success: false, message: 'No matching channel was found'});
|
||||
}
|
||||
res.status(200).json({success: true, data});
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('api error getting channel contents', error);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
app.post('/api/claim-get-long-id', ({ ip, originalUrl, body, params }, res) => {
|
||||
app.post('/api/claim/long-id', ({ ip, originalUrl, body, params }, res) => {
|
||||
logger.debug('body:', body);
|
||||
const channelName = body.channelName;
|
||||
const channelClaimId = body.channelClaimId;
|
||||
|
@ -233,32 +209,45 @@ module.exports = (app) => {
|
|||
getClaimId(channelName, channelClaimId, claimName, claimId)
|
||||
.then(result => {
|
||||
if (result === NO_CHANNEL) {
|
||||
return res.status(200).json({success: false, message: 'No matching channel could be found'});
|
||||
return res.status(404).json({success: false, message: 'No matching channel could be found'});
|
||||
}
|
||||
if (result === NO_CLAIM) {
|
||||
return res.status(200).json({success: false, message: 'No matching claim id could be found'});
|
||||
return res.status(404).json({success: false, message: 'No matching claim id could be found'});
|
||||
}
|
||||
res.status(200).json({success: true, data: result});
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('api error getting long claim id', error);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
app.get('/api/claim-get-data/:claimName/:claimId', ({ ip, originalUrl, body, params }, res) => {
|
||||
app.get('/api/claim/data/:claimName/:claimId', ({ ip, originalUrl, body, params }, res) => {
|
||||
const claimName = params.claimName;
|
||||
let claimId = params.claimId;
|
||||
if (claimId === 'none') claimId = null;
|
||||
db.Claim.resolveClaim(claimName, claimId)
|
||||
.then(claimInfo => {
|
||||
if (!claimInfo) {
|
||||
return res.status(200).json({success: false, message: 'No claim could be found'});
|
||||
return res.status(404).json({success: false, message: 'No claim could be found'});
|
||||
}
|
||||
res.status(200).json({success: true, message: claimInfo});
|
||||
res.status(200).json({success: true, data: claimInfo});
|
||||
})
|
||||
.catch(error => {
|
||||
logger.error('api error getting long claim id', error);
|
||||
errorHandlers.handleApiError(originalUrl, ip, error, res);
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
// route to see if asset is available locally
|
||||
app.get('/api/file/availability/:name/:claimId', ({ ip, originalUrl, params }, res) => {
|
||||
const name = params.name;
|
||||
const claimId = params.claimId;
|
||||
db.File.findOne({where: {name, claimId}})
|
||||
.then(result => {
|
||||
if (result) {
|
||||
return res.status(200).json({success: true, data: true});
|
||||
}
|
||||
res.status(200).json({success: true, data: false});
|
||||
})
|
||||
.catch(error => {
|
||||
errorHandlers.handleErrorResponse(originalUrl, ip, error, res);
|
||||
});
|
||||
});
|
||||
};
|
||||
|
|
|
@ -20,7 +20,7 @@ module.exports = (app) => {
|
|||
return next(err);
|
||||
}
|
||||
if (!user) {
|
||||
return res.status(200).json({
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: info.message,
|
||||
});
|
||||
|
@ -49,7 +49,7 @@ module.exports = (app) => {
|
|||
if (req.user) {
|
||||
res.status(200).json({success: true, data: req.user});
|
||||
} else {
|
||||
res.status(200).json({success: false, message: 'user is not logged in'});
|
||||
res.status(401).json({success: false, message: 'user is not logged in'});
|
||||
}
|
||||
});
|
||||
};
|
||||
|
|
9
routes/fallback-routes.js
Normal file
9
routes/fallback-routes.js
Normal 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);
|
||||
});
|
||||
};
|
|
@ -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');
|
||||
});
|
||||
};
|
|
@ -1,24 +1,29 @@
|
|||
const { site } = require('../config/speechConfig.js');
|
||||
const handlePageRender = require('../helpers/handlePageRender.jsx');
|
||||
|
||||
module.exports = (app) => {
|
||||
// route for the home page
|
||||
app.get('/', (req, res) => {
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to display login page
|
||||
app.get('/login', (req, res) => {
|
||||
res.status(200).render('index');
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to show 'about' page
|
||||
app.get('/about', (req, res) => {
|
||||
res.status(200).render('index');
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to display a list of the trending images
|
||||
app.get('/trending', (req, res) => {
|
||||
res.status(301).redirect('/popular');
|
||||
});
|
||||
app.get('/popular', ({ ip, originalUrl }, res) => {
|
||||
res.status(200).render('index');
|
||||
app.get('/popular', (req, res) => {
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to display a list of the trending images
|
||||
app.get('/new', ({ ip, originalUrl }, res) => {
|
||||
res.status(200).render('index');
|
||||
app.get('/new', (req, res) => {
|
||||
handlePageRender(req, res);
|
||||
});
|
||||
// route to send embedable video player (for twitter)
|
||||
app.get('/embed/:claimId/:name', ({ params }, res) => {
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue