Merge pull request #416 from lbryio/speech-as-a-package

Speech as a package
This commit is contained in:
Bill Bittner 2018-04-18 12:47:33 -07:00 committed by GitHub
commit 235be56d8a
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
203 changed files with 1206 additions and 5549 deletions

4
.babelrc Normal file
View file

@ -0,0 +1,4 @@
{
"presets": ["@babel/env", "@babel/react"],
"plugins": ["@babel/plugin-proposal-object-rest-spread"]
}

View file

@ -1,4 +1,5 @@
node_modules/
public/bundle
exports/
index.js
test
test/
server/render/build

12
.gitignore vendored
View file

@ -1,8 +1,4 @@
node_modules/
.idea/
config/mysqlConfig.js
config/siteConfig.js
public/bundle/
index.js
node_modules
.idea
/devConfig/sequelizeCliConfig.js
/devConfig/testingConfig.js

View file

@ -1,12 +0,0 @@
## Common problems and errors
## How to get help?
For live help, you can join [our chat](https://chat.lbry.io) and post in the #speech channel. Please share a detailed message with the issue you are experiencing.
You can also [email LBRY](mailto:help@lbry.io) with questions or issues.
## Report a bug
To report an issue, open an issue directly on GitHub [here](https://github.com/lbryio/spee.ch). We would appreciate a quick search to see if a similar issues already exist, as well. The penalty for not doing so is a mild shaming.

View file

@ -1,12 +0,0 @@
This FAQ section is for the more advanced users or devlopers who would like to learn how spee.ch works under the hood.
## Spee.ch stack
## Spee.ch and LBRY network

View file

@ -1 +0,0 @@

View file

@ -1,54 +1,19 @@
# Spee.ch
Spee.ch is a web app that reads and publishes images and videos to and from the [LBRY](https://lbry.io/) blockchain.
This repo packages the spee.ch server for use with spee.ch implementations.
## Installation
* start mysql
* install mysql
* create a database called `lbry`
* save your connection `username` and `password` someplace handy
* start lbrynet daemon
* install the [`lbry`](https://github.com/lbryio/lbry) daemon
* start the `lbry` daemon
* start spee.ch
* clone this repo
* run `npm install`
* create your own config files in `/config`
* copy `mysqlConfig.js.example`, name it `mysqlConfig.js`, and update its contents.
* copy `siteConfig.js.example`, name it `siteConfig.js`, and update its contents.
* build the app by running `npm run build`
* for development, `npm run build-dev` will build the app and continue to listen for changes, building again when a change is made.
* to start the server, run `npm run start`
* for development, `npm run start-dev` will start the server and continue to listen for changes, restarting the server again whenever a change is made.
* for production, [pm2](http://pm2.keymetrics.io/docs/usage/quick-start/) is a great tool for starting and managing node processes
* visit [localhost:3000](http://localhost:3000) and check out your spee.ch app!
* start spee.ch-sync (optional, recommended)
* Note: this tool will decode blocks from the `lbry` blockchain and update the Claim and Certificate tables in mysql with all the claims from the blockchain. This is not necessary if you only want to host and resolve content published through your version of spee.ch, but it is required if you want to retrieve and host other content from the lbry network.
* install and run this [`speech-sync`](https://github.com/billbitt/spee.ch-sync) tool
visit [lbryio/www.spee.ch](https://github.com/lbryio/www.spee.ch) to get started
## Development & App Structure
* the `client/` folder houses all of the `react` and `redux` code
* `client.js` is the entry point for the react app
* [react components](https://reactjs.org/docs/react-component.html) are located in `client/components`, `client/containers`, and `client/pages`
* `/components` contains the 'dumb' components that receive props (if any) from their parents
* `/containers` contains the 'smart' redux-connected components that receive props from the `redux-store`
* `/pages` contains the components which act as the main pages of the app
* actions are located in the `client/actions` folder
* reducers are located in the `client/reducers` folder
* sagas are located in the `client/sagas` folder
* the `server/` folder contains all of server code
* `server.js` is the entry point for the server. It creates the [express app](https://expressjs.com/), requires the routes, syncs the database, and starts the server listening on the `PORT` designated in the config file. This file is the entry point for webpack to build the server bundle.
* the `/routes` folder contains all of the routes for the express app
* the `/models` folder contains all of the models which the app uses to interact with the `mysql` database. Note: this app uses the [sequelize](http://docs.sequelizejs.com/) orm.
## Development / Structure
* the `server/` folder contains all of the server code
* `index.js` is the entry point for the server. It creates the [express app](https://expressjs.com/), requires the routes, syncs the database, and starts the server listening on the `PORT` designated in the config file. This file is the entry point for webpack to build the server bundle.
* the `server/routes` folder contains all of the routes for the express app
* the `server/models` folder contains all of the models which the app uses to interact with the `mysql` database. Note: this app uses the [sequelize](http://docs.sequelizejs.com/) ORM.
* webpack
* During the build process, webpack creates two bundles for this project:
* (1) a client-side app bundle which will be located at `public/bundle/bundle.js`
* (2) a server bundle which will be located at `index.js`
* configuration
* the `config/` folder contains all of the required config files. The project contains `.example` files which can be copied to create the necessary `.js` files
* the `devConfig/` folder contains optional config files. Updating these files is not necessary. If you update these files, make sure to add them to your `.gitignore` file so they are not included in source control.
* During the build process, webpack creates a bundle for this project at `index.js`:
## Tests
* Spee.ch uses `mocha` with `chai` for testing.
* This package uses `mocha` with `chai` for testing.
* To run all tests that do not require LBC, run `npm test -- --grep @usesLbc --invert`
* To run all tests, including those that require LBC (like publishing), simply run `npm test`
@ -82,3 +47,23 @@ Spee.ch is a web app that reads and publishes images and videos to and from the
## Bugs
If you find a bug or experience a problem, please report your issue here on github and find us in the lbry discord!
## Contribute
### Below is a guide to the issue tags in this repo
#### level 1
Issues with spee.ch that anyone with basic web development can handle, little-to-no experience with the spee.ch codebase is required.
#### level 2
Issues with spee.ch familiarity with the spee.ch codebase is required, but little-to-no familiarity with the lbry daemon is necessary
#### level 3
Issues with spee.ch strong familiarity with the spee.ch code base and how the lbry daemon functions is required
#### level 4
Issues with lbry (e.g. the spee.ch wallet, lbrynet configuration, etc.) that require strong familiarity with the lbry daemon and/or network to fix. Generally these issues are best suited for the lbry protocol team but are placed in this repo because of they are part of the spee.ch implementation
### Stack
The spee.ch stack is MySQL, Express.js, Node.js, React.js. Spee.ch runs lbrynet on its server, and spee.ch uses the lbrynet api to make requests such as `publish`, `create_channel`, and `get`.
spee.ch also runs a sync tool, which decodes the `LBRY` blocks as they are mined and stores the claims in mysql. It stores all claims in the `Claims` table, and all channel claims in the `Certificates` table.

View file

@ -1,14 +0,0 @@
import * as actions from 'constants/channel_action_types';
// export action creators
export function updateLoggedInChannel (name, shortId, longId) {
return {
type: actions.CHANNEL_UPDATE,
data: {
name,
shortId,
longId,
},
};
};

View file

@ -1,87 +0,0 @@
import * as actions from 'constants/publish_action_types';
// export action creators
export function selectFile (file) {
return {
type: actions.FILE_SELECTED,
data: file,
};
};
export function clearFile () {
return {
type: actions.FILE_CLEAR,
};
};
export function updateMetadata (name, value) {
return {
type: actions.METADATA_UPDATE,
data: {
name,
value,
},
};
};
export function updateClaim (value) {
return {
type: actions.CLAIM_UPDATE,
data: value,
};
};
export function setPublishInChannel (channel) {
return {
type: actions.SET_PUBLISH_IN_CHANNEL,
channel,
};
};
export function updatePublishStatus (status, message) {
return {
type: actions.PUBLISH_STATUS_UPDATE,
data: {
status,
message,
},
};
};
export function updateError (name, value) {
return {
type: actions.ERROR_UPDATE,
data: {
name,
value,
},
};
};
export function updateSelectedChannel (channelName) {
return {
type: actions.SELECTED_CHANNEL_UPDATE,
data: channelName,
};
};
export function toggleMetadataInputs (showMetadataInputs) {
return {
type: actions.TOGGLE_METADATA_INPUTS,
data: showMetadataInputs,
};
};
export function onNewThumbnail (file) {
return {
type: actions.THUMBNAIL_NEW,
data: file,
};
};
export function startPublish (history) {
return {
type: actions.PUBLISH_START,
data: { history },
};
}

View file

@ -1,119 +0,0 @@
import * as actions from 'constants/show_action_types';
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
// basic request parsing
export function onHandleShowPageUri (params) {
return {
type: actions.HANDLE_SHOW_URI,
data: params,
};
};
export function onRequestError (error) {
return {
type: actions.REQUEST_ERROR,
data: error,
};
};
export function onNewChannelRequest (channelName, channelId) {
const requestType = CHANNEL;
const requestId = `cr#${channelName}#${channelId}`;
return {
type: actions.CHANNEL_REQUEST_NEW,
data: { requestType, requestId, channelName, channelId },
};
};
export function onNewAssetRequest (name, id, channelName, channelId, extension) {
const requestType = extension ? ASSET_LITE : ASSET_DETAILS;
const requestId = `ar#${name}#${id}#${channelName}#${channelId}`;
return {
type: actions.ASSET_REQUEST_NEW,
data: {
requestType,
requestId,
name,
modifier: {
id,
channel: {
name: channelName,
id : channelId,
},
},
},
};
};
export function onRequestUpdate (requestType, requestId) {
return {
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,
};
};

View file

@ -1,34 +0,0 @@
import Request from 'utils/request';
export function getLongClaimId (host, 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 (host, name, claimId) {
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
return Request(url);
};
export function getClaimData (host, name, claimId) {
const url = `${host}/api/claim/data/${name}/${claimId}`;
return Request(url);
};

View file

@ -1,13 +0,0 @@
import Request from 'utils/request';
export function getChannelData (host, id, name) {
if (!id) id = 'none';
const url = `${host}/api/channel/data/${name}/${id}`;
return Request(url);
};
export function getChannelClaims (host, longId, name, page) {
if (!page) page = 1;
const url = `${host}/api/channel/claims/${name}/${longId}/${page}`;
return Request(url);
};

View file

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

View file

@ -1,22 +0,0 @@
import React from 'react';
import { Route, Switch } from 'react-router-dom';
import HomePage from 'pages/HomePage'; // or use the provided local homepage
import AboutPage from 'pages/AboutPage';
import LoginPage from 'pages/LoginPage';
import ShowPage from 'pages/ShowPage';
import FourOhFourPage from 'containers/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;

View file

@ -1,48 +0,0 @@
import {buffers, END, eventChannel} from 'redux-saga';
export const makePublishRequestChannel = (fd) => {
return eventChannel(emitter => {
const uri = '/api/claim/publish';
const xhr = new XMLHttpRequest();
// add event listeners
const onLoadStart = () => {
emitter({loadStart: true});
};
const onProgress = (event) => {
if (event.lengthComputable) {
const percentage = Math.round((event.loaded * 100) / event.total);
emitter({progress: percentage});
}
};
const onLoad = () => {
emitter({load: true});
};
xhr.upload.addEventListener('loadstart', onLoadStart);
xhr.upload.addEventListener('progress', onProgress);
xhr.upload.addEventListener('load', onLoad);
// set state change handler
xhr.onreadystatechange = () => {
if (xhr.readyState === 4) {
const response = JSON.parse(xhr.response);
if ((xhr.status === 200) && response.success) {
emitter({success: response});
emitter(END);
} else {
emitter({error: new Error(response.message)});
emitter(END);
}
}
};
// open and send
xhr.open('POST', uri, true);
xhr.send(fd);
// clean up
return () => {
xhr.upload.removeEventListener('loadstart', onLoadStart);
xhr.upload.removeEventListener('progress', onProgress);
xhr.upload.removeEventListener('load', onLoad);
xhr.onreadystatechange = null;
xhr.abort();
};
}, buffers.sliding(2));
};

View file

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

View file

@ -1,7 +0,0 @@
import React from 'react';
const ActiveStatusBar = () => {
return <span className='progress-bar progress-bar--active'>| </span>;
};
export default ActiveStatusBar;

View file

@ -1,10 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({site: {defaults: { defaultThumbnail }}}) => {
return {
defaultThumbnail,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,42 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
const AssetPreview = ({ defaultThumbnail, claimData: { name, claimId, fileExt, contentType, thumbnail } }) => {
const directSourceLink = `${claimId}/${name}.${fileExt}`;
const showUrlLink = `/${claimId}/${name}`;
return (
<div className='asset-holder'>
<Link to={showUrlLink} >
{(() => {
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
case 'image/gif':
return (
<img
className={'asset-preview'}
src={directSourceLink}
alt={name}
/>
);
case 'video/mp4':
return (
<img
className={'asset-preview video'}
src={thumbnail || defaultThumbnail}
alt={name}
/>
);
default:
return (
<p>unsupported file type</p>
);
}
})()}
</Link>
</div>
);
};
export default AssetPreview;

View file

@ -1,37 +0,0 @@
import React, { Component } from 'react';
import PropTypes from 'prop-types';
class ExpandingTextarea extends Component {
constructor (props) {
super(props);
this._handleChange = this._handleChange.bind(this);
}
componentDidMount () {
this.adjustTextarea({});
}
_handleChange (event) {
const { onChange } = this.props;
if (onChange) onChange(event);
this.adjustTextarea(event);
}
adjustTextarea ({ target = this.el }) {
target.style.height = 0;
target.style.height = `${target.scrollHeight}px`;
}
render () {
const { ...rest } = this.props;
return (
<textarea
{...rest}
ref={x => this.el = x}
onChange={this._handleChange}
/>
);
}
}
ExpandingTextarea.propTypes = {
onChange: PropTypes.func,
};
export default ExpandingTextarea;

View file

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

View file

@ -1,7 +0,0 @@
import React from 'react';
const InactiveStatusBar = () => {
return <span className='progress-bar progress-bar--inactive'>| </span>;
};
export default InactiveStatusBar;

View file

@ -1,29 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
function Logo () {
return (
<svg version='1.1' id='Layer_1' x='0px' y='0px' height='24px' viewBox='0 0 80 31' enableBackground='new 0 0 80 31' className='nav-bar-logo'>
<Link to='/'>
<title>Logo</title>
<desc>Spee.ch logo</desc>
<g id='About'>
<g id='Publish-Form-V2-_x28_filled_x29_' transform='translate(-42.000000, -23.000000)'>
<g id='Group-17' transform='translate(42.000000, 22.000000)'>
<text transform='matrix(1 0 0 1 0 20)' fontSize='25' fontFamily='Roboto'>Spee&lt;h</text>
<g id='Group-16' transform='translate(0.000000, 30.000000)'>
<path id='Line-8' fill='none' stroke='#09F911' strokeWidth='1' strokeLinecap='square' d='M0.5,1.5h15' />
<path id='Line-8-Copy' fill='none' stroke='#029D74' strokeWidth='1' strokeLinecap='square' d='M16.5,1.5h15' />
<path id='Line-8-Copy-2' fill='none' stroke='#E35BD8' strokeWidth='1' strokeLinecap='square' d='M32.5,1.5h15' />
<path id='Line-8-Copy-3' fill='none' stroke='#4156C5' strokeWidth='1' strokeLinecap='square' d='M48.5,1.5h15' />
<path id='Line-8-Copy-4' fill='none' stroke='#635688' strokeWidth='1' strokeLinecap='square' d='M64.5,1.5h15' />
</g>
</g>
</g>
</g>
</Link>
</svg>
);
};
export default Logo;

View file

@ -1,13 +0,0 @@
import React from 'react';
function NavBarChannelDropdown ({ channelName, handleSelection, defaultSelection, VIEW, LOGOUT }) {
return (
<select type='text' id='nav-bar-channel-select' className='select select--arrow link--nav' onChange={handleSelection} value={defaultSelection}>
<option id='nav-bar-channel-select-channel-option'>{channelName}</option>
<option value={VIEW}>View</option>
<option value={LOGOUT}>Logout</option>
</select>
);
};
export default NavBarChannelDropdown;

View file

@ -1,76 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import ActiveStatusBar from 'components/ActiveStatusBar';
import InactiveStatusBar from 'components/InactiveStatusBar';
class ProgressBar extends React.Component {
constructor (props) {
super(props);
this.state = {
bars : [],
index : 0,
incrementer: 1,
};
this.createBars = this.createBars.bind(this);
this.startProgressBar = this.startProgressBar.bind(this);
this.updateProgressBar = this.updateProgressBar.bind(this);
this.stopProgressBar = this.stopProgressBar.bind(this);
}
componentDidMount () {
this.createBars();
this.startProgressBar();
}
componentWillUnmount () {
this.stopProgressBar();
}
createBars () {
const bars = [];
for (let i = 0; i <= this.props.size; i++) {
bars.push({isActive: false});
}
this.setState({ bars });
}
startProgressBar () {
this.updateInterval = setInterval(this.updateProgressBar.bind(this), 300);
};
updateProgressBar () {
let index = this.state.index;
let incrementer = this.state.incrementer;
let bars = this.state.bars;
// flip incrementer if necessary, to stay in bounds
if ((index < 0) || (index > this.props.size)) {
incrementer = incrementer * -1;
index += incrementer;
}
// update the indexed bar
if (incrementer > 0) {
bars[index].isActive = true;
} else {
bars[index].isActive = false;
};
// increment index
index += incrementer;
// update state
this.setState({
bars,
incrementer,
index,
});
};
stopProgressBar () {
clearInterval(this.updateInterval);
};
render () {
return (
<div>
{this.state.bars.map((bar, index) => bar.isActive ? <ActiveStatusBar key={index} /> : <InactiveStatusBar key={index}/>)}
</div>
);
}
};
ProgressBar.propTypes = {
size: PropTypes.number.isRequired,
};
export default ProgressBar;

View file

@ -1,62 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
class PublishPreview extends React.Component {
constructor (props) {
super(props);
this.state = {
imgSource : '',
defaultThumbnail: '/assets/img/video_thumb_default.png',
};
}
componentDidMount () {
this.setPreviewImageSource(this.props.file);
}
componentWillReceiveProps (newProps) {
if (newProps.file !== this.props.file) {
this.setPreviewImageSource(newProps.file);
}
if (newProps.thumbnail !== this.props.thumbnail) {
if (newProps.thumbnail) {
this.setPreviewImageSourceFromFile(newProps.thumbnail);
} else {
this.setState({imgSource: this.state.defaultThumbnail});
}
}
}
setPreviewImageSourceFromFile (file) {
const previewReader = new FileReader();
previewReader.readAsDataURL(file);
previewReader.onloadend = () => {
this.setState({imgSource: previewReader.result});
};
}
setPreviewImageSource (file) {
if (file.type !== 'video/mp4') {
this.setPreviewImageSourceFromFile(file);
} else {
if (this.props.thumbnail) {
this.setPreviewImageSourceFromFile(this.props.thumbnail);
}
this.setState({imgSource: this.state.defaultThumbnail});
}
}
render () {
return (
<img
id='dropzone-preview'
src={this.state.imgSource}
className={this.props.dimPreview ? 'dim' : ''}
alt='publish preview'
/>
);
}
};
PublishPreview.propTypes = {
dimPreview: PropTypes.bool.isRequired,
file : PropTypes.object.isRequired,
thumbnail : PropTypes.object,
};
export default PublishPreview;

View file

@ -1,23 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
function UrlMiddle ({publishInChannel, selectedChannel, loggedInChannelName, loggedInChannelShortId}) {
if (publishInChannel) {
if (selectedChannel === loggedInChannelName) {
return <span id='url-channel' className='url-text--secondary'>{loggedInChannelName}:{loggedInChannelShortId} /</span>;
}
return <span id='url-channel-placeholder' className='url-text--secondary tooltip'>@channel<span
className='tooltip-text'>Select a channel below</span> /</span>;
}
return (
<span id='url-no-channel-placeholder' className='url-text--secondary tooltip'>xyz<span className='tooltip-text'>This will be a random id</span> /</span>
);
}
UrlMiddle.propTypes = {
publishInChannel : PropTypes.bool.isRequired,
loggedInChannelName : PropTypes.string,
loggedInChannelShortId: PropTypes.string,
};
export default UrlMiddle;

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ site }) => {
const { defaultDescription, defaultThumbnail, description: siteDescription, host: siteHost, title: siteTitle, twitter: siteTwitter } = site;
return {
defaultDescription,
defaultThumbnail,
siteDescription,
siteHost,
siteTitle,
siteTwitter,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,38 +0,0 @@
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 () {
// props from state
const { defaultDescription, defaultThumbnail, siteDescription, siteHost, siteTitle, siteTwitter } = this.props;
// props from parent
const { asset, channel, pageUri } = this.props;
let { pageTitle } = this.props;
// create page title, tags, and canonical link
pageTitle = createPageTitle(siteTitle, pageTitle);
const metaTags = createMetaTags(siteDescription, siteHost, siteTitle, siteTwitter, asset, channel, defaultDescription, defaultThumbnail);
const canonicalLink = createCanonicalLink(asset, channel, pageUri, siteHost);
// render results
return (
<Helmet
title={pageTitle}
meta={metaTags}
link={[{rel: 'canonical', href: canonicalLink}]}
/>
);
}
};
SEO.propTypes = {
pageTitle: PropTypes.string,
pageUri : PropTypes.string,
channel : PropTypes.object,
asset : PropTypes.object,
};
export default SEO;

View file

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

View file

@ -1 +0,0 @@
export const CHANNEL_UPDATE = 'CHANNEL_UPDATE';

View file

@ -1,11 +0,0 @@
export const FILE_SELECTED = 'FILE_SELECTED';
export const FILE_CLEAR = 'FILE_CLEAR';
export const METADATA_UPDATE = 'METADATA_UPDATE';
export const CLAIM_UPDATE = 'CLAIM_UPDATE';
export const SET_PUBLISH_IN_CHANNEL = 'SET_PUBLISH_IN_CHANNEL';
export const PUBLISH_STATUS_UPDATE = 'PUBLISH_STATUS_UPDATE';
export const ERROR_UPDATE = 'ERROR_UPDATE';
export const SELECTED_CHANNEL_UPDATE = 'SELECTED_CHANNEL_UPDATE';
export const TOGGLE_METADATA_INPUTS = 'TOGGLE_METADATA_INPUTS';
export const THUMBNAIL_NEW = 'THUMBNAIL_NEW';
export const PUBLISH_START = 'PUBLISH_START';

View file

@ -1,2 +0,0 @@
export const LOGIN = 'Existing';
export const CREATE = 'New';

View file

@ -1,5 +0,0 @@
export const LOAD_START = 'LOAD_START';
export const LOADING = 'LOADING';
export const PUBLISHING = 'PUBLISHING';
export const SUCCESS = 'SUCCESS';
export const FAILED = 'FAILED';

View file

@ -1,21 +0,0 @@
// request actions
export const HANDLE_SHOW_URI = 'HANDLE_SHOW_URI';
export const REQUEST_ERROR = 'REQUEST_ERROR';
export const REQUEST_UPDATE = 'REQUEST_UPDATE';
export const ASSET_REQUEST_NEW = 'ASSET_REQUEST_NEW';
export const CHANNEL_REQUEST_NEW = 'CHANNEL_REQUEST_NEW';
export const REQUEST_LIST_ADD = 'REQUEST_LIST_ADD';
// asset actions
export const ASSET_ADD = `ASSET_ADD`;
// channel actions
export const CHANNEL_ADD = 'CHANNEL_ADD';
export const CHANNEL_CLAIMS_UPDATE_ASYNC = 'CHANNEL_CLAIMS_UPDATE_ASYNC';
export const CHANNEL_CLAIMS_UPDATE_SUCCESS = 'CHANNEL_CLAIMS_UPDATE_SUCCESS';
// asset/file display actions
export const FILE_REQUESTED = 'FILE_REQUESTED';
export const FILE_AVAILABILITY_UPDATE = 'FILE_AVAILABILITY_UPDATE';
export const DISPLAY_ASSET_ERROR = 'DISPLAY_ASSET_ERROR';

View file

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

View file

@ -1,28 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
import { fileRequested } from 'actions/show';
import { selectAsset } from 'selectors/show';
const mapStateToProps = ({ show }) => {
// select error and status
const error = show.displayAsset.error;
const status = show.displayAsset.status;
// select asset
const asset = selectAsset(show);
// return props
return {
error,
status,
asset,
};
};
const mapDispatchToProps = dispatch => {
return {
onFileRequest: (name, claimId) => {
dispatch(fileRequested(name, claimId));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,73 +0,0 @@
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 className='asset video' controls poster={thumbnail}>
<source
src={`/${claimId}/${name}.${fileExt}`}
/>
<p>Your browser does not support the <code>video</code> element.</p>
</video>
);
default:
return (
<p>Unsupported file type</p>
);
}
})()
}
</div>
);
}
};
export default AssetDisplay;

View file

@ -1,14 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from 'selectors/show';
const mapStateToProps = ({ show }) => {
// select asset
const asset = selectAsset(show);
// return props
return {
asset,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,123 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
class AssetInfo extends React.Component {
constructor (props) {
super(props);
this.copyToClipboard = this.copyToClipboard.bind(this);
}
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 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--8 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>
<div className='row row--padded row--wide row--no-top'>
<div id='show-short-link'>
<div className='column column--2 column--med-10'>
<span className='text'>Link:</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-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 className='column column--2'>
<button className='button--primary button--wide' 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 className='column column--2'>
<button className='button--primary button--wide' data-elementtocopy='embed-text'
onClick={this.copyToClipboard}>copy
</button>
</div>
</div>
</div>
</div>
</div>
<div className='flex-container--row flex-container--space-between-bottom'>
<Link className='link--primary' to={`/${shortId}/${name}.${fileExt}`}><span
className='text'>Direct Link</span></Link>
<a className='link--primary' href={`${host}/${claimId}/${name}.${fileExt}`} download={name}>Download</a>
<a className='link--primary' target='_blank' href='https://lbry.io/dmca'>Report</a>
</div>
</div>
);
}
};
export default AssetInfo;

View file

@ -1,12 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
import { selectAsset } from 'selectors/show';
const mapStateToProps = ({ show }) => {
const { claimData: { title } } = selectAsset(show);
return {
title,
};
};
export default connect(mapStateToProps, null)(View);

View file

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

View file

@ -1,22 +0,0 @@
import { connect } from 'react-redux';
import { onUpdateChannelClaims } from 'actions/show';
import View from './view';
const mapStateToProps = ({ show }) => {
// select channel key
const request = show.requestList[show.request.id];
const channelKey = request.key;
// select channel claims
const channel = show.channelList[channelKey] || null;
// return props
return {
channelKey,
channel,
};
};
const mapDispatchToProps = {
onUpdateChannelClaims,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,51 +0,0 @@
import React from 'react';
import AssetPreview from 'components/AssetPreview';
class ChannelClaimsDisplay extends React.Component {
constructor (props) {
super(props);
this.showNextResultsPage = this.showNextResultsPage.bind(this);
this.showPreviousResultsPage = this.showPreviousResultsPage.bind(this);
}
showPreviousResultsPage () {
const { channel: { claimsData: { currentPage } } } = this.props;
const previousPage = parseInt(currentPage) - 1;
this.showNewPage(previousPage);
}
showNextResultsPage () {
const { channel: { claimsData: { currentPage } } } = this.props;
const nextPage = parseInt(currentPage) + 1;
this.showNewPage(nextPage);
}
showNewPage (page) {
const { channelKey, channel: { name, longId } } = this.props;
this.props.onUpdateChannelClaims(channelKey, name, longId, page);
}
render () {
const { channel: { claimsData: { claims, currentPage, totalPages } } } = this.props;
return (
<div className='row row--tall'>
{(claims.length > 0) ? (
<div>
{claims.map((claim, index) => <AssetPreview
claimData={claim}
key={`${claim.name}-${index}`}
/>)}
<div>
{(currentPage > 1) &&
<button className={'button--secondary'} onClick={this.showPreviousResultsPage}>Previous Page</button>
}
{(currentPage < totalPages) &&
<button className={'button--secondary'} onClick={this.showNextResultsPage}>Next Page</button>
}
</div>
</div>
) : (
<p>There are no claims in this channel</p>
)}
</div>
);
}
};
export default ChannelClaimsDisplay;

View file

@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import { updateLoggedInChannel } from 'actions/channel';
import View from './view';
import {updateSelectedChannel} from 'actions/publish';
const mapDispatchToProps = dispatch => {
return {
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
dispatch(updateSelectedChannel(name));
},
};
};
export default connect(null, mapDispatchToProps)(View);

View file

@ -1,147 +0,0 @@
import React from 'react';
import ProgressBar from 'components/ProgressBar';
import request from 'utils/request';
class ChannelCreateForm extends React.Component {
constructor (props) {
super(props);
this.state = {
error : null,
channel : '',
password: '',
status : null,
};
this.handleChannelInput = this.handleChannelInput.bind(this);
this.handleInput = this.handleInput.bind(this);
this.createChannel = this.createChannel.bind(this);
}
cleanseChannelInput (input) {
input = input.replace(/\s+/g, '-'); // replace spaces with dashes
input = input.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
return input;
}
handleChannelInput (event) {
let value = event.target.value;
value = this.cleanseChannelInput(value);
this.setState({channel: value});
if (value) {
this.updateIsChannelAvailable(value);
} else {
this.setState({error: 'Please enter a channel name'});
}
}
handleInput (event) {
const name = event.target.name;
const value = event.target.value;
this.setState({[name]: value});
}
updateIsChannelAvailable (channel) {
const channelWithAtSymbol = `@${channel}`;
request(`/api/channel/availability/${channelWithAtSymbol}`)
.then(() => {
this.setState({'error': null});
})
.catch((error) => {
this.setState({'error': error.message});
});
}
checkIsChannelAvailable (channel) {
const channelWithAtSymbol = `@${channel}`;
return request(`/api/channel/availability/${channelWithAtSymbol}`);
}
checkIsPasswordProvided (password) {
return new Promise((resolve, reject) => {
if (!password || password.length < 1) {
return reject(new Error('Please provide a password'));
}
resolve();
});
}
makePublishChannelRequest (username, password) {
const params = {
method : 'POST',
body : JSON.stringify({username, password}),
headers: new Headers({
'Content-Type': 'application/json',
}),
credentials: 'include',
};
return new Promise((resolve, reject) => {
request('/signup', params)
.then(result => {
return resolve(result);
})
.catch(error => {
reject(new Error(`Unfortunately, we encountered an error while creating your channel. Please let us know in Discord! ${error.message}`));
});
});
}
createChannel (event) {
event.preventDefault();
this.checkIsPasswordProvided(this.state.password)
.then(() => {
return this.checkIsChannelAvailable(this.state.channel);
})
.then(() => {
this.setState({status: 'We are publishing your new channel. Sit tight...'});
return this.makePublishChannelRequest(this.state.channel, this.state.password);
})
.then(result => {
this.setState({status: null});
this.props.onChannelLogin(result.channelName, result.shortChannelId, result.channelClaimId);
})
.catch((error) => {
if (error.message) {
this.setState({'error': error.message, status: null});
} else {
this.setState({'error': error, status: null});
};
});
}
render () {
return (
<div>
{ !this.state.status ? (
<form id='publish-channel-form'>
<div className='row row--wide row--short'>
<div className='column column--3 column--sml-10'>
<label className='label' htmlFor='new-channel-name'>Name:</label>
</div><div className='column column--6 column--sml-10'>
<div className='input-text--primary flex-container--row flex-container--left-bottom span--relative'>
<span>@</span>
<input type='text' name='channel' id='new-channel-name' className='input-text' placeholder='exampleChannelName' value={this.state.channel} onChange={this.handleChannelInput} />
{ (this.state.channel && !this.state.error) && <span id='input-success-channel-name' className='info-message--success span--absolute'>{'\u2713'}</span> }
{ this.state.error && <span id='input-success-channel-name' className='info-message--failure span--absolute'>{'\u2716'}</span> }
</div>
</div>
</div>
<div className='row row--wide row--short'>
<div className='column column--3 column--sml-10'>
<label className='label' htmlFor='new-channel-password'>Password:</label>
</div><div className='column column--6 column--sml-10'>
<div className='input-text--primary'>
<input type='password' name='password' id='new-channel-password' className='input-text' placeholder='' value={this.state.password} onChange={this.handleInput} />
</div>
</div>
</div>
{this.state.error ? (
<p className='info-message--failure'>{this.state.error}</p>
) : (
<p className='info-message'>Choose a name and password for your channel</p>
)}
<div className='row row--wide'>
<button className='button--primary' onClick={this.createChannel}>Create Channel</button>
</div>
</form>
) : (
<div>
<p className='fine-print'>{this.state.status}</p>
<ProgressBar size={12} />
</div>
)}
</div>
);
}
}
export default ChannelCreateForm;

View file

@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import { updateLoggedInChannel } from 'actions/channel';
import View from './view';
import {updateSelectedChannel} from '../../actions/publish';
const mapDispatchToProps = dispatch => {
return {
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
dispatch(updateSelectedChannel(name));
},
};
};
export default connect(null, mapDispatchToProps)(View);

View file

@ -1,81 +0,0 @@
import React from 'react';
import request from 'utils/request';
class ChannelLoginForm extends React.Component {
constructor (props) {
super(props);
this.state = {
error : null,
name : '',
password: '',
};
this.handleInput = this.handleInput.bind(this);
this.loginToChannel = this.loginToChannel.bind(this);
}
handleInput (event) {
const name = event.target.name;
const value = event.target.value;
this.setState({[name]: value});
}
loginToChannel (event) {
event.preventDefault();
const params = {
method : 'POST',
body : JSON.stringify({username: this.state.name, password: this.state.password}),
headers: new Headers({
'Content-Type': 'application/json',
}),
credentials: 'include',
};
request('login', params)
.then(({success, channelName, shortChannelId, channelClaimId, message}) => {
if (success) {
this.props.onChannelLogin(channelName, shortChannelId, channelClaimId);
} else {
this.setState({'error': message});
};
})
.catch(error => {
if (error.message) {
this.setState({'error': error.message});
} else {
this.setState({'error': error});
}
});
}
render () {
return (
<form id='channel-login-form'>
<div className='row row--wide row--short'>
<div className='column column--3 column--sml-10'>
<label className='label' htmlFor='channel-login-name-input'>Name:</label>
</div><div className='column column--6 column--sml-10'>
<div className='input-text--primary flex-container--row flex-container--left-bottom'>
<span>@</span>
<input type='text' id='channel-login-name-input' className='input-text' name='name' placeholder='Your Channel Name' value={this.state.channelName} onChange={this.handleInput} />
</div>
</div>
</div>
<div className='row row--wide row--short'>
<div className='column column--3 column--sml-10'>
<label className='label' htmlFor='channel-login-password-input' >Password:</label>
</div><div className='column column--6 column--sml-10'>
<div className='input-text--primary'>
<input type='password' id='channel-login-password-input' name='password' className='input-text' placeholder='' value={this.state.channelPassword} onChange={this.handleInput} />
</div>
</div>
</div>
{ this.state.error ? (
<p className='info-message--failure'>{this.state.error}</p>
) : (
<p className='info-message'>Enter the name and password for your channel</p>
)}
<div className='row row--wide'>
<button className='button--primary' onClick={this.loginToChannel}>Authenticate</button>
</div>
</form>
);
}
}
export default ChannelLoginForm;

View file

@ -1,27 +0,0 @@
import {connect} from 'react-redux';
import {setPublishInChannel, updateSelectedChannel, updateError} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ channel, publish }) => {
return {
loggedInChannelName: channel.loggedInChannel.name,
publishInChannel : publish.publishInChannel,
selectedChannel : publish.selectedChannel,
channelError : publish.error.channel,
};
};
const mapDispatchToProps = dispatch => {
return {
onPublishInChannelChange: (value) => {
dispatch(updateError('channel', null));
dispatch(setPublishInChannel(value));
},
onChannelSelect: (value) => {
dispatch(updateError('channel', null));
dispatch(updateSelectedChannel(value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,62 +0,0 @@
import React from 'react';
import ChannelLoginForm from 'containers/ChannelLoginForm';
import ChannelCreateForm from 'containers/ChannelCreateForm';
import * as states from 'constants/publish_channel_select_states';
class ChannelSelect extends React.Component {
constructor (props) {
super(props);
this.toggleAnonymousPublish = this.toggleAnonymousPublish.bind(this);
this.handleSelection = this.handleSelection.bind(this);
}
toggleAnonymousPublish (event) {
const value = event.target.value;
if (value === 'anonymous') {
this.props.onPublishInChannelChange(false);
} else {
this.props.onPublishInChannelChange(true);
}
}
handleSelection (event) {
const selectedOption = event.target.selectedOptions[0].value;
this.props.onChannelSelect(selectedOption);
}
render () {
return (
<div>
<form>
<div className='column column--3 column--med-10'>
<input type='radio' name='anonymous-or-channel' id='anonymous-radio' className='input-radio' value='anonymous' checked={!this.props.publishInChannel} onChange={this.toggleAnonymousPublish} />
<label className='label label--pointer' htmlFor='anonymous-radio'>Anonymous</label>
</div>
<div className='column column--7 column--med-10'>
<input type='radio' name='anonymous-or-channel' id='channel-radio' className='input-radio' value='in a channel' checked={this.props.publishInChannel} onChange={this.toggleAnonymousPublish} />
<label className='label label--pointer' htmlFor='channel-radio'>In a channel</label>
</div>
{ this.props.channelError ? (
<p className='info-message--failure'>{this.props.channelError}</p>
) : (
<p className='info-message'>Publish anonymously or in a channel</p>
)}
</form>
{ this.props.publishInChannel && (
<div>
<div className='column column--3'>
<label className='label' htmlFor='channel-name-select'>Channel:</label>
</div><div className='column column--7'>
<select type='text' id='channel-name-select' className='select select--arrow' value={this.props.selectedChannel} onChange={this.handleSelection}>
{ this.props.loggedInChannelName && <option value={this.props.loggedInChannelName} id='publish-channel-select-channel-option'>{this.props.loggedInChannelName}</option> }
<option value={states.LOGIN}>Existing</option>
<option value={states.CREATE}>New</option>
</select>
</div>
{ (this.props.selectedChannel === states.LOGIN) && <ChannelLoginForm /> }
{ (this.props.selectedChannel === states.CREATE) && <ChannelCreateForm /> }
</div>
)}
</div>
);
}
}
export default ChannelSelect;

View file

@ -1,25 +0,0 @@
import { connect } from 'react-redux';
import { selectFile, updateError, clearFile } from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
file : publish.file,
thumbnail: publish.thumbnail,
fileError: publish.error.file,
};
};
const mapDispatchToProps = dispatch => {
return {
selectFile: (file) => {
dispatch(selectFile(file));
},
setFileError: (value) => {
dispatch(clearFile());
dispatch(updateError('file', value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,137 +0,0 @@
import React from 'react';
import { validateFile } from 'utils/file';
import PublishPreview from 'components/PublishPreview';
class Dropzone extends React.Component {
constructor (props) {
super(props);
this.state = {
dragOver : false,
mouseOver : false,
dimPreview: false,
};
this.handleDrop = this.handleDrop.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleDragEnter = this.handleDragEnter.bind(this);
this.handleDragLeave = this.handleDragLeave.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleFileInput = this.handleFileInput.bind(this);
this.chooseFile = this.chooseFile.bind(this);
}
handleDrop (event) {
event.preventDefault();
this.setState({dragOver: false});
// if dropped items aren't files, reject them
const dt = event.dataTransfer;
if (dt.items) {
if (dt.items[0].kind === 'file') {
const droppedFile = dt.items[0].getAsFile();
this.chooseFile(droppedFile);
}
}
}
handleDragOver (event) {
event.preventDefault();
}
handleDragEnd (event) {
var dt = event.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
event.dataTransfer.clearData();
}
}
handleDragEnter () {
this.setState({dragOver: true, dimPreview: true});
}
handleDragLeave () {
this.setState({dragOver: false, dimPreview: false});
}
handleMouseEnter () {
this.setState({mouseOver: true, dimPreview: true});
}
handleMouseLeave () {
this.setState({mouseOver: false, dimPreview: false});
}
handleClick (event) {
event.preventDefault();
document.getElementById('file_input').click();
}
handleFileInput (event) {
event.preventDefault();
const fileList = event.target.files;
this.chooseFile(fileList[0]);
}
chooseFile (file) {
if (file) {
try {
validateFile(file); // validate the file's name, type, and size
} catch (error) {
return this.props.setFileError(error.message);
}
// stage it so it will be ready when the publish button is clicked
this.props.selectFile(file);
}
}
render () {
return (
<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' />
</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}>
{this.props.file ? (
<div>
<PublishPreview
dimPreview={this.state.dimPreview}
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>
</div>
) : (
<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>
) : (
<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>
)}
</div>
)}
</div>
</div>
);
}
};
export default Dropzone;

View file

@ -1,11 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ site: { host, title } }) => {
return {
host,
title,
};
};
export default connect(mapStateToProps, null)(View);

View file

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

View file

@ -1,27 +0,0 @@
import { connect } from 'react-redux';
import { updateLoggedInChannel } from 'actions/channel';
import {updateSelectedChannel} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ channel, site }) => {
return {
channelName : channel.loggedInChannel.name,
channelShortId: channel.loggedInChannel.shortId,
channelLongId : channel.loggedInChannel.longId,
siteDescription: site.description,
};
};
const mapDispatchToProps = dispatch => {
return {
onChannelLogin: (name, shortId, longId) => {
dispatch(updateLoggedInChannel(name, shortId, longId));
dispatch(updateSelectedChannel(name));
},
onChannelLogout: () => {
dispatch(updateLoggedInChannel(null, null, null));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,85 +0,0 @@
import React from 'react';
import { NavLink, withRouter } from 'react-router-dom';
import Logo from 'components/Logo';
import NavBarChannelDropdown from 'components/NavBarChannelOptionsDropdown';
import request from 'utils/request';
const VIEW = 'VIEW';
const LOGOUT = 'LOGOUT';
class NavBar extends React.Component {
constructor (props) {
super(props);
this.checkForLoggedInUser = this.checkForLoggedInUser.bind(this);
this.logoutUser = this.logoutUser.bind(this);
this.handleSelection = this.handleSelection.bind(this);
}
componentDidMount () {
// check to see if the user is already logged in
this.checkForLoggedInUser();
}
checkForLoggedInUser () {
const params = {credentials: 'include'};
request('/user', params)
.then(({ data }) => {
this.props.onChannelLogin(data.channelName, data.shortChannelId, data.channelClaimId);
})
.catch(error => {
console.log('/user error:', error.message);
});
}
logoutUser () {
const params = {credentials: 'include'};
request('/logout', params)
.then(() => {
this.props.onChannelLogout();
})
.catch(error => {
console.log('/logout error', error.message);
});
}
handleSelection (event) {
const value = event.target.selectedOptions[0].value;
switch (value) {
case LOGOUT:
this.logoutUser();
break;
case VIEW:
// redirect to channel page
this.props.history.push(`/${this.props.channelName}:${this.props.channelLongId}`);
break;
default:
break;
}
}
render () {
const { siteDescription } = this.props;
return (
<div className='row row--wide nav-bar'>
<div className='row row--padded row--short flex-container--row flex-container--space-between-center'>
<Logo />
<div className='nav-bar--center'>
<span className='nav-bar-tagline'>{siteDescription}</span>
</div>
<div className='nav-bar--right'>
<NavLink className='nav-bar-link link--nav' activeClassName='link--nav-active' to='/' exact>Publish</NavLink>
<NavLink className='nav-bar-link link--nav' activeClassName='link--nav-active' to='/about'>About</NavLink>
{ this.props.channelName ? (
<NavBarChannelDropdown
channelName={this.props.channelName}
handleSelection={this.handleSelection}
defaultSelection={this.props.channelName}
VIEW={VIEW}
LOGOUT={LOGOUT}
/>
) : (
<NavLink id='nav-bar-login-link' className='nav-bar-link link--nav' activeClassName='link--nav-active' to='/login'>Channel</NavLink>
)}
</div>
</div>
</div>
);
}
}
export default withRouter(NavBar);

View file

@ -1,16 +0,0 @@
import {connect} from 'react-redux';
import {clearFile, startPublish} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ channel, publish }) => {
return {
file: publish.file,
};
};
const mapDispatchToProps = {
clearFile,
startPublish,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,63 +0,0 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import Dropzone from 'containers/Dropzone';
import PublishTitleInput from 'containers/PublishTitleInput';
import PublishUrlInput from 'containers/PublishUrlInput';
import PublishThumbnailInput from 'containers/PublishThumbnailInput';
import PublishMetadataInputs from 'containers/PublishMetadataInputs';
import ChannelSelect from 'containers/ChannelSelect';
class PublishDetails extends React.Component {
constructor (props) {
super(props)
this.onPublishSubmit = this.onPublishSubmit.bind(this);
}
onPublishSubmit () {
this.props.startPublish(this.props.history);
}
render () {
return (
<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'>
<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'>
<PublishUrlInput />
</div>
<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 '>
<PublishThumbnailInput />
</div>
)}
<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.onPublishSubmit}>Publish</button>
</div>
<div className='row row--padded row--no-bottom align-content-center'>
<button className='button--cancel' onClick={this.props.clearFile}>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>
</div>
</div>
</div>
);
}
};
export default withRouter(PublishDetails);

View file

@ -1,10 +0,0 @@
import {connect} from 'react-redux';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
message: publish.disabledMessage,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,16 +0,0 @@
import React from 'react';
class PublishDisabledMessage extends React.Component {
render () {
const message = this.props.message;
console.log('this.props.message:', message);
return (
<div className='row dropzone--disabled row--tall flex-container--column flex-container--center-center'>
<p className='text--disabled'>Publishing is currently disabled.</p>
<p className='text--disabled'>{message}</p>
</div>
);
}
}
export default PublishDisabledMessage;

View file

@ -1,25 +0,0 @@
import {connect} from 'react-redux';
import {updateMetadata, toggleMetadataInputs} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
showMetadataInputs: publish.showMetadataInputs,
description : publish.metadata.description,
license : publish.metadata.license,
nsfw : publish.metadata.nsfw,
};
};
const mapDispatchToProps = dispatch => {
return {
onMetadataChange: (name, value) => {
dispatch(updateMetadata(name, value));
},
onToggleMetadataInputs: (value) => {
dispatch(toggleMetadataInputs(value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,74 +0,0 @@
import React from 'react';
import ExpandingTextArea from 'components/ExpandingTextArea';
class PublishMetadataInputs extends React.Component {
constructor (props) {
super(props);
this.toggleShowInputs = this.toggleShowInputs.bind(this);
this.handleInput = this.handleInput.bind(this);
this.handleSelect = this.handleSelect.bind(this);
}
toggleShowInputs () {
this.props.onToggleMetadataInputs(!this.props.showMetadataInputs);
}
handleInput (event) {
const target = event.target;
const value = target.type === 'checkbox' ? target.checked : target.value;
const name = target.name;
this.props.onMetadataChange(name, value);
}
handleSelect (event) {
const name = event.target.name;
const selectedOption = event.target.selectedOptions[0].value;
this.props.onMetadataChange(name, selectedOption);
}
render () {
return (
<div id='publish-details' className='row row--padded row--no-top row--wide'>
{this.props.showMetadataInputs && (
<div>
<div className='row row--no-top'>
<div className='column column--3 column--med-10 align-content-top'>
<label htmlFor='publish-license' className='label'>Description:</label>
</div><div className='column column--7 column--sml-10'>
<ExpandingTextArea
id='publish-description'
className='textarea textarea--primary textarea--full-width'
rows={1}
maxLength={2000}
style={{ maxHeight: 200 }}
name='description'
placeholder='Optional description'
value={this.props.description}
onChange={this.handleInput} />
</div>
</div>
<div className='row row--no-top'>
<div className='column column--3 column--med-10'>
<label htmlFor='publish-license' className='label'>License:</label>
</div><div className='column column--7 column--sml-10'>
<select type='text' name='license' id='publish-license' className='select select--primary' onChange={this.handleSelect}>
<option value=' '>Unspecified</option>
<option value='Public Domain'>Public Domain</option>
<option value='Creative Commons'>Creative Commons</option>
</select>
</div>
</div>
<div className='row row--no-top'>
<div className='column column--3'>
<label htmlFor='publish-nsfw' className='label'>Mature:</label>
</div><div className='column column--7'>
<input className='input-checkbox' type='checkbox' id='publish-nsfw' name='nsfw' value={this.props.nsfw} onChange={this.handleInput} />
</div>
</div>
</div>
)}
<button className='button--secondary' onClick={this.toggleShowInputs}>{this.props.showMetadataInputs ? 'less' : 'more'}</button>
</div>
);
}
}
export default PublishMetadataInputs;

View file

@ -1,16 +0,0 @@
import {connect} from 'react-redux';
import {clearFile} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
status : publish.status.status,
message: publish.status.message,
};
};
const mapDispatchToProps = {
clearFile,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,50 +0,0 @@
import React from 'react';
import ProgressBar from 'components/ProgressBar';
import * as publishStates from 'constants/publish_claim_states';
class PublishStatus extends React.Component {
render () {
const { status, message, clearFile } = this.props;
return (
<div className='row row--tall flex-container--column flex-container--center-center'>
{status === publishStates.LOAD_START &&
<div className='row align-content-center'>
<p>File is loading to server</p>
<p className='blue'>0%</p>
</div>
}
{status === publishStates.LOADING &&
<div>
<div className='row align-content-center'>
<p>File is loading to server</p>
<p className='blue'>{message}</p>
</div>
</div>
}
{status === publishStates.PUBLISHING &&
<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>
</div>
}
{status === publishStates.SUCCESS &&
<div className='row align-content-center'>
<p>Your publish is complete! You are being redirected to it now.</p>
<p>If you are not automatically redirected, <a className='link--primary' target='_blank' href={message}>click here.</a></p>
</div>
}
{status === publishStates.FAILED &&
<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>
<button className='button--secondary' onClick={clearFile}>Reset</button>
</div>
}
</div>
);
}
};
export default PublishStatus;

View file

@ -1,15 +0,0 @@
import { connect } from 'react-redux';
import { onNewThumbnail } from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish: { file } }) => {
return {
file,
};
};
const mapDispatchToProps = {
onNewThumbnail,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,140 +0,0 @@
import React from 'react';
function dataURItoBlob(dataURI) {
// convert base64/URLEncoded data component to raw binary data held in a string
let byteString = atob(dataURI.split(',')[1]);
// separate out the mime component
let mimeString = dataURI.split(',')[0].split(':')[1].split(';')[0];
// write the bytes of the string to a typed array
let ia = new Uint8Array(byteString.length);
for (let i = 0; i < byteString.length; i++) {
ia[i] = byteString.charCodeAt(i);
}
return new Blob([ia], {type: mimeString});
}
class PublishThumbnailInput extends React.Component {
constructor (props) {
super(props);
this.state = {
videoSource : null,
error : null,
sliderMinRange: 1,
sliderMaxRange: null,
sliderValue : null,
};
this.handleVideoLoadedData = this.handleVideoLoadedData.bind(this);
this.handleSliderChange = this.handleSliderChange.bind(this);
this.createThumbnail = this.createThumbnail.bind(this);
}
componentDidMount () {
const { file } = this.props;
this.setVideoSource(file);
}
componentWillReceiveProps (nextProps) {
// if file changes
if (nextProps.file && nextProps.file !== this.props.file) {
const { file } = nextProps;
this.setVideoSource(file);
};
}
setVideoSource (file) {
const previewReader = new FileReader();
previewReader.readAsDataURL(file);
previewReader.onloadend = () => {
const dataUri = previewReader.result;
const blob = dataURItoBlob(dataUri);
const videoSource = URL.createObjectURL(blob);
this.setState({ videoSource });
};
}
handleVideoLoadedData (event) {
const duration = event.target.duration;
const totalMinutes = Math.floor(duration / 60);
const totalSeconds = Math.floor(duration % 60);
// set the slider
this.setState({
sliderMaxRange: duration * 100,
sliderValue : duration * 100 / 2,
totalMinutes,
totalSeconds,
});
// update the current time of the video
let video = document.getElementById('video-thumb-player');
video.currentTime = duration / 2;
}
handleSliderChange (event) {
const value = parseInt(event.target.value);
// update the slider value
this.setState({
sliderValue: value,
});
// update the current time of the video
let video = document.getElementById('video-thumb-player');
video.currentTime = value / 100;
}
createThumbnail () {
// take a snapshot
let video = document.getElementById('video-thumb-player');
let canvas = document.createElement('canvas');
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
canvas.getContext('2d').drawImage(video, 0, 0, canvas.width, canvas.height);
const dataUrl = canvas.toDataURL();
const blob = dataURItoBlob(dataUrl);
const snapshot = new File([blob], `thumbnail.png`, {
type: 'image/png',
});
// set the thumbnail in redux store
if (snapshot) {
this.props.onNewThumbnail(snapshot);
}
}
render () {
const { error, videoSource, sliderMinRange, sliderMaxRange, sliderValue, totalMinutes, totalSeconds } = this.state;
return (
<div>
<label className='label'>Thumbnail:</label>
<video
id='video-thumb-player'
preload='metadata'
muted
style={{display: 'none'}}
playsInline
onLoadedData={this.handleVideoLoadedData}
src={videoSource}
onSeeked={this.createThumbnail}
/>
{
sliderValue ? (
<div>
<div className='flex-container--row flex-container--space-between-center' style={{width: '100%'}}>
<span className='info-message'>0'00"</span>
<span className='info-message'>{totalMinutes}'{totalSeconds}"</span>
</div>
<div>
<input
type='range'
min={sliderMinRange}
max={sliderMaxRange}
value={sliderValue}
className='slider'
onChange={this.handleSliderChange}
/>
</div>
</div>
) : (
<p className='info-message' >loading... </p>
)
}
{ error ? (
<p className='info-message--failure'>{error}</p>
) : (
<p className='info-message'>Use slider to set thumbnail</p>
)}
</div>
);
}
}
export default PublishThumbnailInput;

View file

@ -1,19 +0,0 @@
import {connect} from 'react-redux';
import {updateMetadata} from 'actions/publish';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
title: publish.metadata.title,
};
};
const mapDispatchToProps = dispatch => {
return {
onMetadataChange: (name, value) => {
dispatch(updateMetadata(name, value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,20 +0,0 @@
import React from 'react';
class PublishTitleInput extends React.Component {
constructor (props) {
super(props);
this.handleInput = this.handleInput.bind(this);
}
handleInput (e) {
const name = e.target.name;
const value = e.target.value;
this.props.onMetadataChange(name, value);
}
render () {
return (
<input type='text' id='publish-title' className='input-text text--large input-text--full-width' name='title' placeholder='Give your post a title...' onChange={this.handleInput} value={this.props.title} />
);
}
}
export default PublishTitleInput;

View file

@ -1,12 +0,0 @@
import {connect} from 'react-redux';
import View from './view';
const mapStateToProps = ({ publish }) => {
return {
disabled: publish.disabled,
file : publish.file,
status : publish.status.status,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,30 +0,0 @@
import React from 'react';
import Dropzone from 'containers/Dropzone';
import PublishDetails from 'containers/PublishDetails';
import PublishStatus from 'containers/PublishStatus';
import PublishDisabledMessage from 'containers/PublishDisabledMessage';
class PublishTool extends React.Component {
render () {
if (this.props.disabled) {
console.log('publish is disabled');
return (
<PublishDisabledMessage />
);
} else {
console.log('publish is not disabled');
if (this.props.file) {
if (this.props.status) {
return (
<PublishStatus />
);
} else {
return <PublishDetails />;
}
}
return <Dropzone />;
}
}
};
export default PublishTool;

View file

@ -1,29 +0,0 @@
import {updateClaim, updateError} from 'actions/publish';
import {connect} from 'react-redux';
import View from './view';
const mapStateToProps = ({ channel, publish }) => {
return {
loggedInChannelName : channel.loggedInChannel.name,
loggedInChannelShortId: channel.loggedInChannel.shortId,
fileName : publish.file.name,
publishInChannel : publish.publishInChannel,
selectedChannel : publish.selectedChannel,
claim : publish.claim,
urlError : publish.error.url,
};
};
const mapDispatchToProps = dispatch => {
return {
onClaimChange: (value) => {
dispatch(updateClaim(value));
dispatch(updateError('publishSubmit', null));
},
onUrlError: (value) => {
dispatch(updateError('url', value));
},
};
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,82 +0,0 @@
import React from 'react';
import request from 'utils/request';
import UrlMiddle from 'components/PublishUrlMiddleDisplay';
class PublishUrlInput extends React.Component {
constructor (props) {
super(props);
this.handleInput = this.handleInput.bind(this);
}
componentDidMount () {
const { claim, fileName } = this.props;
if (!claim) {
this.setClaimName(fileName);
}
}
componentWillReceiveProps ({ claim, fileName }) {
// if a new file was chosen, update the claim name
if (fileName !== this.props.fileName) {
return this.setClaimName(fileName);
}
// if the claim has updated, check its availability
if (claim !== this.props.claim) {
this.validateClaim(claim);
}
}
handleInput (event) {
let value = event.target.value;
value = this.cleanseInput(value);
// update the state
this.props.onClaimChange(value);
}
cleanseInput (input) {
input = input.replace(/\s+/g, '-'); // replace spaces with dashes
input = input.replace(/[^A-Za-z0-9-]/g, ''); // remove all characters that are not A-Z, a-z, 0-9, or '-'
return input;
}
setClaimName (fileName) {
const fileNameWithoutEnding = fileName.substring(0, fileName.lastIndexOf('.'));
const cleanClaimName = this.cleanseInput(fileNameWithoutEnding);
this.props.onClaimChange(cleanClaimName);
}
validateClaim (claim) {
if (!claim) {
return this.props.onUrlError('Enter a url above');
}
request(`/api/claim/availability/${claim}`)
.then(() => {
this.props.onUrlError(null);
})
.catch((error) => {
this.props.onUrlError(error.message);
});
}
render () {
const { claim, loggedInChannelName, loggedInChannelShortId, publishInChannel, selectedChannel, urlError } = this.props;
return (
<div className='column column--10 column--sml-10'>
<div className='input-text--primary span--relative'>
<span className='url-text--secondary'>spee.ch / </span>
<UrlMiddle
publishInChannel={publishInChannel}
selectedChannel={selectedChannel}
loggedInChannelName={loggedInChannelName}
loggedInChannelShortId={loggedInChannelShortId}
/>
<input type='text' id='claim-name-input' className='input-text' name='claim' placeholder='your-url-here' onChange={this.handleInput} value={claim} />
{ (claim && !urlError) && <span id='input-success-claim-name' className='info-message--success span--absolute'>{'\u2713'}</span> }
{ urlError && <span id='input-success-channel-name' className='info-message--failure span--absolute'>{'\u2716'}</span> }
</div>
<div>
{ urlError ? (
<p id='input-error-claim-name' className='info-message--failure'>{urlError}</p>
) : (
<p className='info-message'>Choose a custom url</p>
)}
</div>
</div>
);
}
}
export default PublishUrlInput;

View file

@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ show }) => {
// select request info
const requestId = show.request.id;
// select asset info
let asset;
const request = show.requestList[requestId] || null;
const assetList = show.assetList;
if (request && assetList) {
const assetKey = request.key; // note: just store this in the request
asset = assetList[assetKey] || null;
};
// return props
return {
asset,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,41 +0,0 @@
import React from 'react';
import SEO from 'components/SEO';
import NavBar from 'containers/NavBar';
import ErrorPage from 'pages/ErrorPage';
import AssetTitle from 'containers/AssetTitle';
import AssetDisplay from 'containers/AssetDisplay';
import AssetInfo from 'containers/AssetInfo';
class ShowAssetDetails extends React.Component {
render () {
const { asset } = this.props;
if (asset) {
const { claimData: { name } } = asset;
return (
<div>
<SEO pageTitle={`${name} - details`} asset={asset} />
<NavBar />
<div className='row row--tall row--padded'>
<div className='column column--10'>
<AssetTitle />
</div>
<div className='column column--5 column--sml-10 align-content-top'>
<div className='row row--padded show-details-container'>
<AssetDisplay />
</div>
</div><div className='column column--5 column--sml-10 align-content-top'>
<div className='row row--padded'>
<AssetInfo />
</div>
</div>
</div>
</div>
);
};
return (
<ErrorPage error={'loading asset data...'} />
);
}
};
export default ShowAssetDetails;

View file

@ -1,21 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ show }) => {
// select request info
const requestId = show.request.id;
// select asset info
let asset;
const request = show.requestList[requestId] || null;
const assetList = show.assetList;
if (request && assetList) {
const assetKey = request.key; // note: just store this in the request
asset = assetList[assetKey] || null;
};
// return props
return {
asset,
};
};
export default connect(mapStateToProps, null)(View);

View file

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

View file

@ -1,20 +0,0 @@
import { connect } from 'react-redux';
import View from './view';
const mapStateToProps = ({ show }) => {
// select request info
const requestId = show.request.id;
// select request
const previousRequest = show.requestList[requestId] || null;
// select channel
let channel;
if (previousRequest) {
const channelKey = previousRequest.key;
channel = show.channelList[channelKey] || null;
}
return {
channel,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,35 +0,0 @@
import React from 'react';
import SEO from 'components/SEO';
import ErrorPage from 'pages/ErrorPage';
import NavBar from 'containers/NavBar';
import ChannelClaimsDisplay from 'containers/ChannelClaimsDisplay';
class ShowChannel extends React.Component {
render () {
const { channel } = this.props;
if (channel) {
const { name, longId, shortId } = channel;
return (
<div>
<SEO pageTitle={name} channel={channel} />
<NavBar />
<div className='row row--tall row--padded'>
<div className='column column--10'>
<h2>channel name: {name}</h2>
<p className={'fine-print'}>full channel id: {longId}</p>
<p className={'fine-print'}>short channel id: {shortId}</p>
</div>
<div className='column column--10'>
<ChannelClaimsDisplay />
</div>
</div>
</div>
);
};
return (
<ErrorPage error={'loading channel data...'} />
);
}
};
export default ShowChannel;

View file

@ -1,35 +0,0 @@
import React from 'react';
import NavBar from 'containers/NavBar';
import SEO from 'components/SEO';
class AboutPage extends React.Component {
render () {
return (
<div>
<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>
<p>Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the <a className='link--primary' href='https://lbry.io/get'>LBRY</a> network. This means that your images are stored in multiple locations without a single point of failure.</p>
<h3>Contribute</h3>
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a className='link--primary' href='https://github.com/lbryio/spee.ch'>github repo</a> and go to town!</p>
<p>If you want to improve spee.ch, join our <a className='link--primary' href='https://discord.gg/YjYbwhS'>discord channel</a> or solve one of our <a className='link--primary' href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p>
</div>
</div>
</div>
</div>
);
}
};
export default AboutPage;

View file

@ -1,23 +0,0 @@
import React from 'react';
import PropTypes from 'prop-types';
import NavBar from 'containers/NavBar';
class ErrorPage extends React.Component {
render () {
const { error } = this.props;
return (
<div>
<NavBar />
<div className='row row--padded'>
<p>{error}</p>
</div>
</div>
);
}
};
ErrorPage.propTypes = {
error: PropTypes.string.isRequired,
};
export default ErrorPage;

View file

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

View file

@ -1,10 +0,0 @@
import {connect} from 'react-redux';
import View from './view';
const mapStateToProps = ({ channel }) => {
return {
loggedInChannelName: channel.loggedInChannel.name,
};
};
export default connect(mapStateToProps, null)(View);

View file

@ -1,39 +0,0 @@
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 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) {
this.props.history.push(`/`);
}
}
render () {
return (
<div>
<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>
</div>
);
}
};
export default withRouter(LoginPage);

View file

@ -1,16 +0,0 @@
import { connect } from 'react-redux';
import { onHandleShowPageUri } from 'actions/show';
import View from './view';
const mapStateToProps = ({ show }) => {
return {
error : show.request.error,
requestType: show.request.type,
};
};
const mapDispatchToProps = {
onHandleShowPageUri,
};
export default connect(mapStateToProps, mapDispatchToProps)(View);

View file

@ -1,38 +0,0 @@
import React from 'react';
import ErrorPage from 'pages/ErrorPage';
import ShowAssetLite from 'containers/ShowAssetLite';
import ShowAssetDetails from 'containers/ShowAssetDetails';
import ShowChannel from 'containers/ShowChannel';
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
class ShowPage extends React.Component {
componentDidMount () {
this.props.onHandleShowPageUri(this.props.match.params);
}
componentWillReceiveProps (nextProps) {
if (nextProps.match.params !== this.props.match.params) {
this.props.onHandleShowPageUri(nextProps.match.params);
}
}
render () {
const { error, requestType } = this.props;
if (error) {
return (
<ErrorPage error={error} />
);
}
switch (requestType) {
case CHANNEL:
return <ShowChannel />;
case ASSET_LITE:
return <ShowAssetLite />;
case ASSET_DETAILS:
return <ShowAssetDetails />;
default:
return <p>loading...</p>;
}
}
};
export default ShowPage;

View file

@ -1,20 +0,0 @@
import * as actions from 'constants/channel_action_types';
const initialState = {
loggedInChannel: {
name : null,
shortId: null,
longId : null,
},
};
export default function (state = initialState, action) {
switch (action.type) {
case actions.CHANNEL_UPDATE:
return Object.assign({}, state, {
loggedInChannel: action.data,
});
default:
return state;
}
}

View file

@ -1,12 +0,0 @@
import { combineReducers } from 'redux';
import PublishReducer from 'reducers/publish';
import ChannelReducer from 'reducers/channel';
import ShowReducer from 'reducers/show';
import SiteReducer from 'reducers/site';
export default combineReducers({
channel: ChannelReducer,
publish: PublishReducer,
show : ShowReducer,
site : SiteReducer,
});

View file

@ -1,79 +0,0 @@
import * as actions from 'constants/publish_action_types';
import { LOGIN } from 'constants/publish_channel_select_states';
const { publishing } = require('../../config/siteConfig.js');
const initialState = {
disabled : publishing.disabled,
disabledMessage : publishing.disabledMessage,
publishInChannel : false,
selectedChannel : LOGIN,
showMetadataInputs: false,
status : {
status : null,
message: null,
},
error: {
file : null,
url : null,
channel : null,
publishSubmit: null,
},
file : null,
claim : '',
metadata: {
title : '',
description: '',
license : '',
nsfw : false,
},
thumbnail: null,
};
export default function (state = initialState, action) {
switch (action.type) {
case actions.FILE_SELECTED:
return Object.assign({}, initialState, { // note: clears to initial state
file: action.data,
});
case actions.FILE_CLEAR:
return initialState;
case actions.METADATA_UPDATE:
return Object.assign({}, state, {
metadata: Object.assign({}, state.metadata, {
[action.data.name]: action.data.value,
}),
});
case actions.CLAIM_UPDATE:
return Object.assign({}, state, {
claim: action.data,
});
case actions.SET_PUBLISH_IN_CHANNEL:
return Object.assign({}, state, {
publishInChannel: action.channel,
});
case actions.PUBLISH_STATUS_UPDATE:
return Object.assign({}, state, {
status: action.data,
});
case actions.ERROR_UPDATE:
return Object.assign({}, state, {
error: Object.assign({}, state.error, {
[action.data.name]: action.data.value,
}),
});
case actions.SELECTED_CHANNEL_UPDATE:
return Object.assign({}, state, {
selectedChannel: action.data,
});
case actions.TOGGLE_METADATA_INPUTS:
return Object.assign({}, state, {
showMetadataInputs: action.data,
});
case actions.THUMBNAIL_NEW:
return Object.assign({}, state, {
thumbnail: action.data,
});
default:
return state;
}
}

View file

@ -1,95 +0,0 @@
import * as actions from 'constants/show_action_types';
import { LOCAL_CHECK, ERROR } from 'constants/asset_display_states';
const initialState = {
request: {
error: null,
type : null,
id : null,
},
requestList : {},
channelList : {},
assetList : {},
displayAsset: {
error : null,
status: LOCAL_CHECK,
},
};
export default function (state = initialState, action) {
switch (action.type) {
// handle request
case actions.REQUEST_ERROR:
return Object.assign({}, state, {
request: Object.assign({}, state.request, {
error: action.data,
}),
});
case actions.REQUEST_UPDATE:
return Object.assign({}, state, {
request: Object.assign({}, state.request, {
type: action.data.requestType,
id : action.data.requestId,
}),
});
// store requests
case actions.REQUEST_LIST_ADD:
return Object.assign({}, state, {
requestList: Object.assign({}, state.requestList, {
[action.data.id]: {
error: action.data.error,
key : action.data.key,
},
}),
});
// asset data
case actions.ASSET_ADD:
return Object.assign({}, state, {
assetList: Object.assign({}, state.assetList, {
[action.data.id]: {
error : action.data.error,
name : action.data.name,
claimId : action.data.claimId,
shortId : action.data.shortId,
claimData: action.data.claimData,
},
}),
});
// channel data
case actions.CHANNEL_ADD:
return Object.assign({}, state, {
channelList: Object.assign({}, state.channelList, {
[action.data.id]: {
name : action.data.name,
longId : action.data.longId,
shortId : action.data.shortId,
claimsData: action.data.claimsData,
},
}),
});
case actions.CHANNEL_CLAIMS_UPDATE_SUCCESS:
return Object.assign({}, state, {
channelList: Object.assign({}, state.channelList, {
[action.data.channelListId]: Object.assign({}, state.channelList[action.data.channelListId], {
claimsData: action.data.claimsData,
}),
}),
});
// display an asset
case actions.FILE_AVAILABILITY_UPDATE:
return Object.assign({}, state, {
displayAsset: Object.assign({}, state.displayAsset, {
status: action.data,
}),
});
case actions.DISPLAY_ASSET_ERROR:
return Object.assign({}, state, {
displayAsset: Object.assign({}, state.displayAsset, {
error : action.data,
status: ERROR,
}),
});
default:
return state;
}
}

View file

@ -1,34 +0,0 @@
const siteConfig = require('../../config/siteConfig.js');
const {
analytics: {
googleId: googleAnalyticsId,
},
assetDefaults: {
thumbnail: defaultThumbnail,
description: defaultDescription,
},
details: {
description,
host,
title,
twitter,
},
} = siteConfig;
const initialState = {
description,
googleAnalyticsId,
host,
title,
twitter,
defaultDescription,
defaultThumbnail,
};
export default function (state = initialState, action) {
switch (action.type) {
default:
return state;
}
}

View file

@ -1,35 +0,0 @@
import {call, put, select, 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';
import { selectSiteHost } from 'selectors/site';
function * retrieveFile (action) {
const name = action.data.name;
const claimId = action.data.claimId;
const host = yield select(selectSiteHost);
// see if the file is available
let isAvailable;
try {
({ data: isAvailable } = yield call(checkFileAvailability, claimId, host, name));
} 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, claimId, host, name);
} catch (error) {
return yield put(updateDisplayAssetError(error.message));
};
yield put(updateFileAvailability(AVAILABLE));
};
export function * watchFileIsRequested () {
yield takeLatest(actions.FILE_REQUESTED, retrieveFile);
};

View file

@ -1,17 +0,0 @@
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';
import { watchPublishStart } from './publish';
export default function * rootSaga () {
yield all([
watchHandleShowPageUri(),
watchNewAssetRequest(),
watchNewChannelRequest(),
watchUpdateChannelClaims(),
watchFileIsRequested(),
watchPublishStart(),
]);
}

View file

@ -1,62 +0,0 @@
import { call, put, select, take, takeLatest } from 'redux-saga/effects';
import * as actions from 'constants/publish_action_types';
import * as publishStates from 'constants/publish_claim_states';
import { updateError, updatePublishStatus, clearFile } from 'actions/publish';
import { selectPublishState } from 'selectors/publish';
import { selectChannelState } from 'selectors/channel';
import { selectSiteState } from 'selectors/site';
import { validateChannelSelection, validatePublishParams } from 'utils/validate';
import { createPublishMetadata, createPublishFormData, createThumbnailUrl } from 'utils/publish';
import { makePublishRequestChannel } from 'channels/publish';
function * publishFile (action) {
const { history } = action.data;
const { publishInChannel, selectedChannel, file, claim, metadata, thumbnailChannel, thumbnailChannelId, thumbnail, error: { url: urlError } } = yield select(selectPublishState);
const { loggedInChannel } = yield select(selectChannelState);
const { host } = yield select(selectSiteState);
// validate the channel selection
try {
validateChannelSelection(publishInChannel, selectedChannel, loggedInChannel);
} catch (error) {
return yield put(updateError('channel', error.message));
};
// validate publish parameters
try {
validatePublishParams(file, claim, urlError);
} catch (error) {
return yield put(updateError('publishSubmit', error.message));
}
// create metadata
let publishMetadata = createPublishMetadata(claim, file, metadata, publishInChannel, selectedChannel);
if (thumbnail) {
// add thumbnail to publish metadata
publishMetadata['thumbnail'] = createThumbnailUrl(thumbnailChannel, thumbnailChannelId, claim, host);
}
// create form data for main publish
const publishFormData = createPublishFormData(file, thumbnail, publishMetadata);
// make the publish request
const publishChannel = yield call(makePublishRequestChannel, publishFormData);
while (true) {
const {loadStart, progress, load, success, error} = yield take(publishChannel);
if (error) {
return yield put(updatePublishStatus(publishStates.FAILED, error.message));
}
if (success) {
yield put(clearFile());
return history.push(`/${success.data.claimId}/${success.data.name}`);
}
if (loadStart) {
yield put(updatePublishStatus(publishStates.LOAD_START, null));
}
if (progress) {
yield put(updatePublishStatus(publishStates.LOADING, `${progress}%`));
}
if (load) {
yield put(updatePublishStatus(publishStates.PUBLISHING, null));
}
}
};
export function * watchPublishStart () {
yield takeLatest(actions.PUBLISH_START, publishFile);
};

View file

@ -1,55 +0,0 @@
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';
import { selectSiteHost } from 'selectors/site';
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);
const host = yield select(selectSiteHost);
if (state.requestList[requestId]) {
return null;
}
// get long id && add request to request list
let longId;
try {
({data: longId} = yield call(getLongClaimId, host, name, modifier));
} catch (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]) {
return null;
}
// get short Id
let shortId;
try {
({data: shortId} = yield call(getShortId, host, name, longId));
} catch (error) {
return yield put(onRequestError(error.message));
}
// get asset claim data
let claimData;
try {
({data: claimData} = yield call(getClaimData, host, name, longId));
} catch (error) {
return yield put(onRequestError(error.message));
}
// add asset to asset list
yield put(addAssetToAssetList(assetKey, null, name, longId, shortId, claimData));
// clear any errors in request error
yield put(onRequestError(null));
};
export function * watchNewAssetRequest () {
yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest);
};

View file

@ -1,65 +0,0 @@
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';
import { selectSiteHost } from 'selectors/site';
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);
const host = yield select(selectSiteHost);
if (state.requestList[requestId]) {
return null;
}
// get channel long id
let longId, shortId;
try {
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, host, 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]) {
return null;
}
// get channel claims data
let claimsData;
try {
({ data: claimsData } = yield call(getChannelClaims, host, longId, channelName, 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;
const host = yield select(selectSiteHost);
let claimsData;
try {
({ data: claimsData } = yield call(getChannelClaims, host, longId, name, 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);
}

View file

@ -1,59 +0,0 @@
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) {
// 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) {
// 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) {
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);
};

View file

@ -1,3 +0,0 @@
export const selectChannelState = (state) => {
return state.channel;
};

View file

@ -1,3 +0,0 @@
export const selectPublishState = (state) => {
return state.publish;
};

View file

@ -1,9 +0,0 @@
export const selectAsset = (show) => {
const request = show.requestList[show.request.id];
const assetKey = request.key;
return show.assetList[assetKey];
};
export const selectShowState = (state) => {
return state.show;
};

View file

@ -1,7 +0,0 @@
export const selectSiteState = (state) => {
return state.site;
};
export const selectSiteHost = (state) => {
return state.site.host;
};

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