Speech as a package #416
105 changed files with 0 additions and 3773 deletions
|
@ -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,26 +0,0 @@
|
|||
import React from 'react';
|
||||
import { Route, Switch } from 'react-router-dom';
|
||||
import { dynamicImport } from 'utils/dynamicImport';
|
||||
// import HomePage from 'pages/HomePage';
|
||||
// import AboutPage from 'pages/AboutPage';
|
||||
import LoginPage from 'pages/LoginPage';
|
||||
import ShowPage from 'pages/ShowPage';
|
||||
import FourOhFourPage from 'containers/FourOhFourPage';
|
||||
|
||||
const HomePage = dynamicImport('pages/HomePage') || require('pages/HomePage').default;
|
||||
const AboutPage = dynamicImport('pages/AboutPage') || require('pages/AboutPage').default;
|
||||
|
||||
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,55 +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';
|
||||
|
||||
const siteConfig = require('siteConfig.js');
|
||||
|
||||
// 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 the store
|
||||
let store;
|
||||
if (preloadedState) {
|
||||
store = createStore(Reducer, preloadedState, reduxMiddleware);
|
||||
} else {
|
||||
store = createStore(Reducer, reduxMiddleware);
|
||||
}
|
||||
|
||||
function Client () {
|
||||
this.configureSiteDetails = (userConfig) => {
|
||||
siteConfig.update(userConfig);
|
||||
};
|
||||
this.start = () => {
|
||||
// run the saga middlweare
|
||||
sagaMiddleware.run(rootSaga);
|
||||
|
||||
// render the app
|
||||
hydrate(
|
||||
<Provider store={store}>
|
||||
<BrowserRouter>
|
||||
<GAListener>
|
||||
<App />
|
||||
</GAListener>
|
||||
</BrowserRouter>
|
||||
</Provider>,
|
||||
document.getElementById('react-app')
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
module.exports = Client;
|
|
@ -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('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,12 +0,0 @@
|
|||
const Path = require('path');
|
||||
const { getSubDirectoryNames } = require('tools/getFolderNames.js');
|
||||
|
||||
const thisFolder = Path.resolve(__dirname, 'client/components/');
|
||||
let modules = {};
|
||||
|
||||
getSubDirectoryNames(thisFolder)
|
||||
.forEach((name) => {
|
||||
modules[name] = require(`./${name}`).default;
|
||||
});
|
||||
|
||||
module.exports = modules;
|
|
@ -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,12 +0,0 @@
|
|||
const Path = require('path');
|
||||
const { getSubDirectoryNames } = require('tools/getFolderNames.js');
|
||||
const thisFolder = Path.resolve(__dirname, 'client/containers/');
|
||||
|
||||
let modules = {};
|
||||
|
||||
getSubDirectoryNames(thisFolder)
|
||||
.forEach((name) => {
|
||||
modules[name] = require(`./${name}`).default;
|
||||
});
|
||||
|
||||
module.exports = modules;
|
|
@ -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,12 +0,0 @@
|
|||
const Path = require('path');
|
||||
const { getSubDirectoryNames } = require('tools/getFolderNames.js');
|
||||
const thisFolder = Path.resolve(__dirname, 'client/pages/');
|
||||
|
||||
let modules = {};
|
||||
|
||||
getSubDirectoryNames(thisFolder)
|
||||
.forEach((name) => {
|
||||
modules[name] = require(`./${name}`).default;
|
||||
});
|
||||
|
||||
module.exports = modules;
|
|
@ -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('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('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;
|
||||
};
|
|
@ -1,29 +0,0 @@
|
|||
const createBasicCanonicalLink = (page, siteHost) => {
|
||||
return `${siteHost}/${page}`;
|
||||
};
|
||||
|
||||
const createAssetCanonicalLink = (asset, siteHost) => {
|
||||
let channelName, certificateId, name, claimId;
|
||||
if (asset.claimData) {
|
||||
({ channelName, certificateId, name, claimId } = asset.claimData);
|
||||
};
|
||||
if (channelName) {
|
||||
return `${siteHost}/${channelName}:${certificateId}/${name}`;
|
||||
};
|
||||
return `${siteHost}/${claimId}/${name}`;
|
||||
};
|
||||
|
||||
const createChannelCanonicalLink = (channel, siteHost) => {
|
||||
const { name, longId } = channel;
|
||||
return `${siteHost}/${name}:${longId}`;
|
||||
};
|
||||
|
||||
export const createCanonicalLink = (asset, channel, page, siteHost) => {
|
||||
if (asset) {
|
||||
return createAssetCanonicalLink(asset, siteHost);
|
||||
}
|
||||
if (channel) {
|
||||
return createChannelCanonicalLink(channel, siteHost);
|
||||
}
|
||||
return createBasicCanonicalLink(page, siteHost);
|
||||
};
|
|
@ -1,41 +0,0 @@
|
|||
const { customComponents } = require('siteConfig.js');
|
||||
|
||||
function getDeepestChildValue (parent, childrenKeys) {
|
||||
if (!parent[childrenKeys[0]]) {
|
||||
return null;
|
||||
}
|
||||
let childKey = childrenKeys.shift(); // .shift() retrieves the first element of array and removes it from array
|
||||
let child = parent[childKey];
|
||||
if (childrenKeys.length >= 1) {
|
||||
return getDeepestChildValue(child, childrenKeys);
|
||||
}
|
||||
return child;
|
||||
}
|
||||
|
||||
export const dynamicImport = (filePath) => {
|
||||
// validate inputs
|
||||
if (!filePath) {
|
||||
throw new Error('no file path provided to dynamicImport()');
|
||||
}
|
||||
if (typeof filePath !== 'string') {
|
||||
console.log('dynamicImport > filePath:', filePath);
|
||||
console.log('dynamicImport > filePath type:', typeof filePath);
|
||||
throw new Error('file path provided to dynamicImport() must be a string');
|
||||
}
|
||||
if (!customComponents) {
|
||||
console.log('No customComponents found in siteConfig.js');
|
||||
return null;
|
||||
}
|
||||
// split out the file folders // filter out any empty or white-space-only strings
|
||||
const folders = filePath.split('/').filter(folderName => folderName.replace(/\s/g, '').length);
|
||||
// check for the component corresponding to file path in the site config object
|
||||
// i.e. customComponents[folders[0]][folders[2][...][folders[n]]
|
||||
const component = getDeepestChildValue(customComponents, folders);
|
||||
if (component) {
|
||||
console.log('Found custom component:', component);
|
||||
return component;
|
||||
} else {
|
||||
console.log('Found custom component:', component);
|
||||
return null;
|
||||
}
|
||||
};
|
|
@ -1,32 +0,0 @@
|
|||
module.exports = {
|
||||
validateFile (file) {
|
||||
if (!file) {
|
||||
throw new Error('no file provided');
|
||||
}
|
||||
if (/'/.test(file.name)) {
|
||||
throw new Error('apostrophes are not allowed in the file name');
|
||||
}
|
||||
// validate size and type
|
||||
switch (file.type) {
|
||||
case 'image/jpeg':
|
||||
case 'image/jpg':
|
||||
case 'image/png':
|
||||
if (file.size > 10000000) {
|
||||
throw new Error('Sorry, images are limited to 10 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'image/gif':
|
||||
if (file.size > 50000000) {
|
||||
throw new Error('Sorry, GIFs are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
case 'video/mp4':
|
||||
if (file.size > 50000000) {
|
||||
throw new Error('Sorry, videos are limited to 50 megabytes.');
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new Error(file.type + ' is not a supported file type. Only, .jpeg, .png, .gif, and .mp4 files are currently supported.');
|
||||
}
|
||||
},
|
||||
};
|
|
@ -1,85 +0,0 @@
|
|||
module.exports = {
|
||||
REGEXP_INVALID_CLAIM : /[^A-Za-z0-9-]/g,
|
||||
REGEXP_INVALID_CHANNEL: /[^A-Za-z0-9-@]/g,
|
||||
REGEXP_ADDRESS : /^b(?=[^0OIl]{32,33})[0-9A-Za-z]{32,33}$/,
|
||||
CHANNEL_CHAR : '@',
|
||||
parseIdentifier : function (identifier) {
|
||||
const componentsRegex = new RegExp(
|
||||
'([^:$#/]*)' + // value (stops at the first separator or end)
|
||||
'([:$#]?)([^/]*)' // modifier separator, modifier (stops at the first path separator or end)
|
||||
);
|
||||
const [proto, value, modifierSeperator, modifier] = componentsRegex // eslint-disable-line no-unused-vars
|
||||
.exec(identifier)
|
||||
.map(match => match || null);
|
||||
|
||||
// Validate and process name
|
||||
if (!value) {
|
||||
throw new Error(`Check your URL. No channel name provided before "${modifierSeperator}"`);
|
||||
}
|
||||
const isChannel = value.startsWith(module.exports.CHANNEL_CHAR);
|
||||
const channelName = isChannel ? value : null;
|
||||
let claimId;
|
||||
if (isChannel) {
|
||||
if (!channelName) {
|
||||
throw new Error('Check your URL. No channel name after "@".');
|
||||
}
|
||||
const nameBadChars = (channelName).match(module.exports.REGEXP_INVALID_CHANNEL);
|
||||
if (nameBadChars) {
|
||||
throw new Error(`Check your URL. Invalid characters in channel name: "${nameBadChars.join(', ')}".`);
|
||||
}
|
||||
} else {
|
||||
claimId = value;
|
||||
}
|
||||
|
||||
// Validate and process modifier
|
||||
let channelClaimId;
|
||||
if (modifierSeperator) {
|
||||
if (!modifier) {
|
||||
throw new Error(`Check your URL. No modifier provided after separator "${modifierSeperator}"`);
|
||||
}
|
||||
|
||||
if (modifierSeperator === ':') {
|
||||
channelClaimId = modifier;
|
||||
} else {
|
||||
throw new Error(`Check your URL. The "${modifierSeperator}" modifier is not currently supported`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
isChannel,
|
||||
channelName,
|
||||
channelClaimId: channelClaimId || null,
|
||||
claimId : claimId || null,
|
||||
};
|
||||
},
|
||||
parseClaim: function (name) {
|
||||
const componentsRegex = new RegExp(
|
||||
'([^:$#/.]*)' + // name (stops at the first extension)
|
||||
'([:$#.]?)([^/]*)' // extension separator, extension (stops at the first path separator or end)
|
||||
);
|
||||
const [proto, claimName, extensionSeperator, extension] = componentsRegex // eslint-disable-line no-unused-vars
|
||||
.exec(name)
|
||||
.map(match => match || null);
|
||||
|
||||
// Validate and process name
|
||||
if (!claimName) {
|
||||
throw new Error('Check your URL. No claim name provided before "."');
|
||||
}
|
||||
const nameBadChars = (claimName).match(module.exports.REGEXP_INVALID_CLAIM);
|
||||
if (nameBadChars) {
|
||||
throw new Error(`Check your URL. Invalid characters in claim name: "${nameBadChars.join(', ')}".`);
|
||||
}
|
||||
// Validate and process extension
|
||||
if (extensionSeperator) {
|
||||
if (!extension) {
|
||||
throw new Error(`Check your URL. No file extension provided after separator "${extensionSeperator}".`);
|
||||
}
|
||||
if (extensionSeperator !== '.') {
|
||||
throw new Error(`Check your URL. The "${extensionSeperator}" separator is not supported in the claim name.`);
|
||||
}
|
||||
}
|
||||
return {
|
||||
claimName,
|
||||
extension: extension || null,
|
||||
};
|
||||
},
|
||||
};
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue