Merge pull request #416 from lbryio/speech-as-a-package
Speech as a package
This commit is contained in:
commit
235be56d8a
203 changed files with 1206 additions and 5549 deletions
4
.babelrc
Normal file
4
.babelrc
Normal file
|
@ -0,0 +1,4 @@
|
|||
{
|
||||
"presets": ["@babel/env", "@babel/react"],
|
||||
"plugins": ["@babel/plugin-proposal-object-rest-spread"]
|
||||
}
|
|
@ -1,4 +1,5 @@
|
|||
node_modules/
|
||||
public/bundle
|
||||
exports/
|
||||
index.js
|
||||
test
|
||||
test/
|
||||
server/render/build
|
||||
|
|
12
.gitignore
vendored
12
.gitignore
vendored
|
@ -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
|
||||
|
|
|
@ -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.
|
|
@ -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
|
||||
|
||||
|
||||
|
|
@ -1 +0,0 @@
|
|||
|
73
README.md
73
README.md
|
@ -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.
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
};
|
|
@ -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 },
|
||||
};
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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;
|
|
@ -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));
|
||||
};
|
|
@ -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')
|
||||
);
|
|
@ -1,7 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const ActiveStatusBar = () => {
|
||||
return <span className='progress-bar progress-bar--active'>| </span>;
|
||||
};
|
||||
|
||||
export default ActiveStatusBar;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -1,7 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const InactiveStatusBar = () => {
|
||||
return <span className='progress-bar progress-bar--inactive'>| </span>;
|
||||
};
|
||||
|
||||
export default InactiveStatusBar;
|
|
@ -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<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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -1,4 +0,0 @@
|
|||
export const LOCAL_CHECK = 'LOCAL_CHECK';
|
||||
export const UNAVAILABLE = 'UNAVAILABLE';
|
||||
export const ERROR = 'ERROR';
|
||||
export const AVAILABLE = 'AVAILABLE';
|
|
@ -1 +0,0 @@
|
|||
export const CHANNEL_UPDATE = 'CHANNEL_UPDATE';
|
|
@ -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';
|
|
@ -1,2 +0,0 @@
|
|||
export const LOGIN = 'Existing';
|
||||
export const CREATE = 'New';
|
|
@ -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';
|
|
@ -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';
|
|
@ -1,3 +0,0 @@
|
|||
export const CHANNEL = 'CHANNEL';
|
||||
export const ASSET_LITE = 'ASSET_LITE';
|
||||
export const ASSET_DETAILS = 'ASSET_DETAILS';
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -1,11 +0,0 @@
|
|||
import React from 'react';
|
||||
|
||||
const AssetTitle = ({ title }) => {
|
||||
return (
|
||||
<div>
|
||||
<span className='text--large'>{title}</span>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default AssetTitle;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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;
|
|
@ -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);
|
|
@ -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);
|
|
@ -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);
|
|
@ -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;
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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,
|
||||
});
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -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(),
|
||||
]);
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
};
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
export const selectChannelState = (state) => {
|
||||
return state.channel;
|
||||
};
|
|
@ -1,3 +0,0 @@
|
|||
export const selectPublishState = (state) => {
|
||||
return state.publish;
|
||||
};
|
|
@ -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;
|
||||
};
|
|
@ -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
Loading…
Reference in a new issue