Speech as a package #416

Merged
bones7242 merged 89 commits from speech-as-a-package into master 2018-04-18 21:47:34 +02:00
105 changed files with 0 additions and 3773 deletions
Showing only changes of commit cb4da07ab5 - Show all commits

View file

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

View file

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

View file

@ -1,119 +0,0 @@
import * as actions from 'constants/show_action_types';
import { CHANNEL, ASSET_LITE, ASSET_DETAILS } from 'constants/show_request_types';
// basic request parsing
export function onHandleShowPageUri (params) {
return {
type: actions.HANDLE_SHOW_URI,
data: params,
};
};
export function onRequestError (error) {
return {
type: actions.REQUEST_ERROR,
data: error,
};
};
export function onNewChannelRequest (channelName, channelId) {
const requestType = CHANNEL;
const requestId = `cr#${channelName}#${channelId}`;
return {
type: actions.CHANNEL_REQUEST_NEW,
data: { requestType, requestId, channelName, channelId },
};
};
export function onNewAssetRequest (name, id, channelName, channelId, extension) {
const requestType = extension ? ASSET_LITE : ASSET_DETAILS;
const requestId = `ar#${name}#${id}#${channelName}#${channelId}`;
return {
type: actions.ASSET_REQUEST_NEW,
data: {
requestType,
requestId,
name,
modifier: {
id,
channel: {
name: channelName,
id : channelId,
},
},
},
};
};
export function onRequestUpdate (requestType, requestId) {
return {
type: actions.REQUEST_UPDATE,
data: {
requestType,
requestId,
},
};
};
export function addRequestToRequestList (id, error, key) {
return {
type: actions.REQUEST_LIST_ADD,
data: { id, error, key },
};
};
// asset actions
export function addAssetToAssetList (id, error, name, claimId, shortId, claimData) {
return {
type: actions.ASSET_ADD,
data: { id, error, name, claimId, shortId, claimData },
};
}
// channel actions
export function addNewChannelToChannelList (id, name, shortId, longId, claimsData) {
return {
type: actions.CHANNEL_ADD,
data: { id, name, shortId, longId, claimsData },
};
};
export function onUpdateChannelClaims (channelKey, name, longId, page) {
return {
type: actions.CHANNEL_CLAIMS_UPDATE_ASYNC,
data: {channelKey, name, longId, page},
};
};
export function updateChannelClaims (channelListId, claimsData) {
return {
type: actions.CHANNEL_CLAIMS_UPDATE_SUCCESS,
data: {channelListId, claimsData},
};
};
// display a file
export function fileRequested (name, claimId) {
return {
type: actions.FILE_REQUESTED,
data: { name, claimId },
};
};
export function updateFileAvailability (status) {
return {
type: actions.FILE_AVAILABILITY_UPDATE,
data: status,
};
};
export function updateDisplayAssetError (error) {
return {
type: actions.DISPLAY_ASSET_ERROR,
data: error,
};
};

View file

@ -1,34 +0,0 @@
import Request from 'utils/request';
export function getLongClaimId (host, name, modifier) {
let body = {};
// create request params
if (modifier) {
if (modifier.id) {
body['claimId'] = modifier.id;
} else {
body['channelName'] = modifier.channel.name;
body['channelClaimId'] = modifier.channel.id;
}
}
body['claimName'] = name;
const params = {
method : 'POST',
headers: { 'Content-Type': 'application/json' },
body : JSON.stringify(body),
};
// create url
const url = `${host}/api/claim/long-id`;
// return the request promise
return Request(url, params);
};
export function getShortId (host, name, claimId) {
const url = `${host}/api/claim/short-id/${claimId}/${name}`;
return Request(url);
};
export function getClaimData (host, name, claimId) {
const url = `${host}/api/claim/data/${name}/${claimId}`;
return Request(url);
};

View file

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

View file

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

View file

@ -1,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;

View file

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

View file

@ -1,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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,73 +0,0 @@
import React from 'react';
import ProgressBar from 'components/ProgressBar';
import { LOCAL_CHECK, UNAVAILABLE, ERROR, AVAILABLE } from 'constants/asset_display_states';
class AssetDisplay extends React.Component {
componentDidMount () {
const { asset: { claimData: { name, claimId } } } = this.props;
this.props.onFileRequest(name, claimId);
}
render () {
const { status, error, asset: { claimData: { name, claimId, contentType, fileExt, thumbnail } } } = this.props;
return (
<div id='asset-display-component'>
{(status === LOCAL_CHECK) &&
<div>
<p>Checking to see if Spee.ch has your asset locally...</p>
</div>
}
{(status === UNAVAILABLE) &&
<div>
<p>Sit tight, we're searching the LBRY blockchain for your asset!</p>
<ProgressBar size={12} />
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
</div>
}
{(status === ERROR) &&
<div>
<p>Unfortunately, we couldn't download your asset from LBRY. You can help us out by sharing the below error message in the <a className='link--primary' href='https://discord.gg/YjYbwhS' target='_blank'>LBRY discord</a>.</p>
<i><p id='error-message'>{error}</p></i>
</div>
}
{(status === AVAILABLE) &&
(() => {
switch (contentType) {
case 'image/jpeg':
case 'image/jpg':
case 'image/png':
return (
<img
className='asset'
src={`/${claimId}/${name}.${fileExt}`}
alt={name} />
);
case 'image/gif':
return (
<img
className='asset'
src={`/${claimId}/${name}.${fileExt}`}
alt={name}
/>
);
case 'video/mp4':
return (
<video className='asset video' controls poster={thumbnail}>
<source
src={`/${claimId}/${name}.${fileExt}`}
/>
<p>Your browser does not support the <code>video</code> element.</p>
</video>
);
default:
return (
<p>Unsupported file type</p>
);
}
})()
}
</div>
);
}
};
export default AssetDisplay;

View file

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

View file

@ -1,123 +0,0 @@
import React from 'react';
import { Link } from 'react-router-dom';
class AssetInfo extends React.Component {
constructor (props) {
super(props);
this.copyToClipboard = this.copyToClipboard.bind(this);
}
copyToClipboard (event) {
var elementToCopy = event.target.dataset.elementtocopy;
var element = document.getElementById(elementToCopy);
element.select();
try {
document.execCommand('copy');
} catch (err) {
this.setState({error: 'Oops, unable to copy'});
}
}
render () {
const { asset: { shortId, claimData : { channelName, certificateId, description, name, claimId, fileExt, contentType, thumbnail, host } } } = this.props;
return (
<div>
{channelName &&
<div className='row row--padded row--wide row--no-top'>
<div className='column column--2 column--med-10'>
<span className='text'>Channel:</span>
</div>
<div className='column column--8 column--med-10'>
<span className='text'><Link to={`/${channelName}:${certificateId}`}>{channelName}</Link></span>
</div>
</div>
}
{description &&
<div className='row row--padded row--wide row--no-top'>
<span className='text'>{description}</span>
</div>
}
<div id='show-share-buttons'>
<div className='row row--padded row--wide row--no-top'>
<div className='column column--2 column--med-10'>
<span className='text'>Share:</span>
</div>
<div className='column column--8 column--med-10'>
<div
className='row row--short row--wide flex-container--row flex-container--space-between-bottom flex-container--wrap'>
<a className='link--primary' target='_blank' href={`https://twitter.com/intent/tweet?text=${host}/${shortId}/${name}`}>twitter</a>
<a className='link--primary' target='_blank' href={`https://www.facebook.com/sharer/sharer.php?u=${host}/${shortId}/${name}`}>facebook</a>
<a className='link--primary' target='_blank' href={`http://tumblr.com/widgets/share/tool?canonicalUrl=${host}/${shortId}/${name}`}>tumblr</a>
<a className='link--primary' target='_blank' href={`https://www.reddit.com/submit?url=${host}/${shortId}/${name}&title=${name}`}>reddit</a>
</div>
</div>
</div>
</div>
<div className='row row--padded row--wide row--no-top'>
<div id='show-short-link'>
<div className='column column--2 column--med-10'>
<span className='text'>Link:</span>
</div>
<div className='column column--8 column--med-10'>
<div className='row row--short row--wide'>
<div className='column column--7'>
<div className='input-error' id='input-error-copy-short-link' hidden='true'>error here</div>
<input type='text' id='short-link' className='input-disabled input-text--full-width' readOnly
spellCheck='false'
value={`${host}/${shortId}/${name}.${fileExt}`}
onClick={this.select} />
</div>
<div className='column column--1' />
<div className='column column--2'>
<button className='button--primary button--wide' data-elementtocopy='short-link'
onClick={this.copyToClipboard}>copy
</button>
</div>
</div>
</div>
</div>
<div id='show-embed-code'>
<div className='column column--2 column--med-10'>
<span className='text'>Embed:</span>
</div>
<div className='column column--8 column--med-10'>
<div className='row row--short row--wide'>
<div className='column column--7'>
<div className='input-error' id='input-error-copy-embed-text' hidden='true'>error here</div>
{(contentType === 'video/mp4') ? (
<input type='text' id='embed-text' className='input-disabled input-text--full-width' readOnly
onClick={this.select} spellCheck='false'
value={`<video width="100%" controls poster="${thumbnail}" src="${host}/${claimId}/${name}.${fileExt}"/></video>`} />
) : (
<input type='text' id='embed-text' className='input-disabled input-text--full-width' readOnly
onClick={this.select} spellCheck='false'
value={`<img src="${host}/${claimId}/${name}.${fileExt}"/>`}
/>
)}
</div>
<div className='column column--1' />
<div className='column column--2'>
<button className='button--primary button--wide' data-elementtocopy='embed-text'
onClick={this.copyToClipboard}>copy
</button>
</div>
</div>
</div>
</div>
</div>
<div className='flex-container--row flex-container--space-between-bottom'>
<Link className='link--primary' to={`/${shortId}/${name}.${fileExt}`}><span
className='text'>Direct Link</span></Link>
<a className='link--primary' href={`${host}/${claimId}/${name}.${fileExt}`} download={name}>Download</a>
<a className='link--primary' target='_blank' href='https://lbry.io/dmca'>Report</a>
</div>
</div>
);
}
};
export default AssetInfo;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,137 +0,0 @@
import React from 'react';
import { validateFile } from 'utils/file';
import PublishPreview from 'components/PublishPreview';
class Dropzone extends React.Component {
constructor (props) {
super(props);
this.state = {
dragOver : false,
mouseOver : false,
dimPreview: false,
};
this.handleDrop = this.handleDrop.bind(this);
this.handleDragOver = this.handleDragOver.bind(this);
this.handleDragEnd = this.handleDragEnd.bind(this);
this.handleDragEnter = this.handleDragEnter.bind(this);
this.handleDragLeave = this.handleDragLeave.bind(this);
this.handleMouseEnter = this.handleMouseEnter.bind(this);
this.handleMouseLeave = this.handleMouseLeave.bind(this);
this.handleClick = this.handleClick.bind(this);
this.handleFileInput = this.handleFileInput.bind(this);
this.chooseFile = this.chooseFile.bind(this);
}
handleDrop (event) {
event.preventDefault();
this.setState({dragOver: false});
// if dropped items aren't files, reject them
const dt = event.dataTransfer;
if (dt.items) {
if (dt.items[0].kind === 'file') {
const droppedFile = dt.items[0].getAsFile();
this.chooseFile(droppedFile);
}
}
}
handleDragOver (event) {
event.preventDefault();
}
handleDragEnd (event) {
var dt = event.dataTransfer;
if (dt.items) {
for (var i = 0; i < dt.items.length; i++) {
dt.items.remove(i);
}
} else {
event.dataTransfer.clearData();
}
}
handleDragEnter () {
this.setState({dragOver: true, dimPreview: true});
}
handleDragLeave () {
this.setState({dragOver: false, dimPreview: false});
}
handleMouseEnter () {
this.setState({mouseOver: true, dimPreview: true});
}
handleMouseLeave () {
this.setState({mouseOver: false, dimPreview: false});
}
handleClick (event) {
event.preventDefault();
document.getElementById('file_input').click();
}
handleFileInput (event) {
event.preventDefault();
const fileList = event.target.files;
this.chooseFile(fileList[0]);
}
chooseFile (file) {
if (file) {
try {
validateFile(file); // validate the file's name, type, and size
} catch (error) {
return this.props.setFileError(error.message);
}
// stage it so it will be ready when the publish button is clicked
this.props.selectFile(file);
}
}
render () {
return (
<div className='row row--tall flex-container--column'>
<form>
<input className='input-file' type='file' id='file_input' name='file_input' accept='video/*,image/*' onChange={this.handleFileInput} encType='multipart/form-data' />
</form>
<div id='preview-dropzone' className={'row row--padded row--tall dropzone' + (this.state.dragOver ? ' dropzone--drag-over' : '')} onDrop={this.handleDrop} onDragOver={this.handleDragOver} onDragEnd={this.handleDragEnd} onDragEnter={this.handleDragEnter} onDragLeave={this.handleDragLeave} onMouseEnter={this.handleMouseEnter} onMouseLeave={this.handleMouseLeave} onClick={this.handleClick}>
{this.props.file ? (
<div>
<PublishPreview
dimPreview={this.state.dimPreview}
file={this.props.file}
thumbnail={this.props.thumbnail}
/>
<div id='dropzone-text-holder' className={'flex-container--column flex-container--center-center'}>
{ this.state.dragOver ? (
<div id='dropzone-dragover'>
<p className='blue'>Drop it.</p>
</div>
) : (
null
)}
{ this.state.mouseOver ? (
<div id='dropzone-instructions'>
<p className='info-message-placeholder info-message--failure' id='input-error-file-selection'>{this.props.fileError}</p>
<p>Drag & drop image or video here to publish</p>
<p className='fine-print'>OR</p>
<p className='blue--underlined'>CHOOSE FILE</p>
</div>
) : (
null
)}
</div>
</div>
) : (
<div id='dropzone-text-holder' className={'flex-container--column flex-container--center-center'}>
{ this.state.dragOver ? (
<div id='dropzone-dragover'>
<p className='blue'>Drop it.</p>
</div>
) : (
<div id='dropzone-instructions'>
<p className='info-message-placeholder info-message--failure' id='input-error-file-selection'>{this.props.fileError}</p>
<p>Drag & drop image or video here to publish</p>
<p className='fine-print'>OR</p>
<p className='blue--underlined'>CHOOSE FILE</p>
</div>
)}
</div>
)}
</div>
</div>
);
}
};
export default Dropzone;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,63 +0,0 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import Dropzone from 'containers/Dropzone';
import PublishTitleInput from 'containers/PublishTitleInput';
import PublishUrlInput from 'containers/PublishUrlInput';
import PublishThumbnailInput from 'containers/PublishThumbnailInput';
import PublishMetadataInputs from 'containers/PublishMetadataInputs';
import ChannelSelect from 'containers/ChannelSelect';
class PublishDetails extends React.Component {
constructor (props) {
super(props)
this.onPublishSubmit = this.onPublishSubmit.bind(this);
}
onPublishSubmit () {
this.props.startPublish(this.props.history);
}
render () {
return (
<div className='row row--no-bottom'>
<div className='column column--10'>
<PublishTitleInput />
</div>
{/* left column */}
<div className='column column--5 column--sml-10' >
<div className='row row--padded'>
<Dropzone />
</div>
</div>
{/* right column */}
<div className='column column--5 column--sml-10 align-content-top'>
<div id='publish-active-area' className='row row--padded'>
<div className='row row--padded row--no-top row--wide'>
<PublishUrlInput />
</div>
<div className='row row--padded row--no-top row--wide'>
<ChannelSelect />
</div>
{ (this.props.file.type === 'video/mp4') && (
<div className='row row--padded row--no-top row--wide '>
<PublishThumbnailInput />
</div>
)}
<div className='row row--padded row--no-top row--no-bottom row--wide'>
<PublishMetadataInputs />
</div>
<div className='row row--wide align-content-center'>
<button id='publish-submit' className='button--primary button--large' onClick={this.onPublishSubmit}>Publish</button>
</div>
<div className='row row--padded row--no-bottom align-content-center'>
<button className='button--cancel' onClick={this.props.clearFile}>Cancel</button>
</div>
<div className='row row--short align-content-center'>
<p className='fine-print'>By clicking 'Publish', you affirm that you have the rights to publish this content to the LBRY network, and that you understand the properties of publishing it to a decentralized, user-controlled network. <a className='link--primary' target='_blank' href='https://lbry.io/learn'>Read more.</a></p>
</div>
</div>
</div>
</div>
);
}
};
export default withRouter(PublishDetails);

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,50 +0,0 @@
import React from 'react';
import ProgressBar from 'components/ProgressBar';
import * as publishStates from 'constants/publish_claim_states';
class PublishStatus extends React.Component {
render () {
const { status, message, clearFile } = this.props;
return (
<div className='row row--tall flex-container--column flex-container--center-center'>
{status === publishStates.LOAD_START &&
<div className='row align-content-center'>
<p>File is loading to server</p>
<p className='blue'>0%</p>
</div>
}
{status === publishStates.LOADING &&
<div>
<div className='row align-content-center'>
<p>File is loading to server</p>
<p className='blue'>{message}</p>
</div>
</div>
}
{status === publishStates.PUBLISHING &&
<div className='row align-content-center'>
<p>Upload complete. Your file is now being published on the blockchain...</p>
<ProgressBar size={12} />
<p>Curious what magic is happening here? <a className='link--primary' target='blank' href='https://lbry.io/faq/what-is-lbry'>Learn more.</a></p>
</div>
}
{status === publishStates.SUCCESS &&
<div className='row align-content-center'>
<p>Your publish is complete! You are being redirected to it now.</p>
<p>If you are not automatically redirected, <a className='link--primary' target='_blank' href={message}>click here.</a></p>
</div>
}
{status === publishStates.FAILED &&
<div className='row align-content-center'>
<p>Something went wrong...</p>
<p><strong>{message}</strong></p>
<p>For help, post the above error text in the #speech channel on the <a className='link--primary' href='https://discord.gg/YjYbwhS' target='_blank'>lbry discord</a></p>
<button className='button--secondary' onClick={clearFile}>Reset</button>
</div>
}
</div>
);
}
};
export default PublishStatus;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,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;

View file

@ -1,35 +0,0 @@
import React from 'react';
import NavBar from 'containers/NavBar';
import SEO from 'components/SEO';
class AboutPage extends React.Component {
render () {
return (
<div>
<SEO pageTitle={'About'} pageUri={'about'} />
<NavBar />
<div className='row row--padded'>
<div className='column column--5 column--med-10 align-content-top'>
<div className='column column--8 column--med-10'>
<p className='pull-quote'>Spee.ch is an open-source project. Please contribute to the existing site, or fork it and make your own.</p>
<p><a className='link--primary' target='_blank' href='https://twitter.com/spee_ch'>TWITTER</a></p>
<p><a className='link--primary' target='_blank' href='https://github.com/lbryio/spee.ch'>GITHUB</a></p>
<p><a className='link--primary' target='_blank' href='https://discord.gg/YjYbwhS'>DISCORD CHANNEL</a></p>
<p><a className='link--primary' target='_blank' href='https://github.com/lbryio/spee.ch/blob/master/README.md'>DOCUMENTATION</a></p>
</div>
</div><div className='column column--5 column--med-10 align-content-top'>
<div className='column column--8 column--med-10'>
<p>Spee.ch is a media-hosting site that reads from and publishes content to the <a className='link--primary' href='https://lbry.io'>LBRY</a> blockchain.</p>
<p>Spee.ch is a hosting service, but with the added benefit that it stores your content on a decentralized network of computers -- the <a className='link--primary' href='https://lbry.io/get'>LBRY</a> network. This means that your images are stored in multiple locations without a single point of failure.</p>
<h3>Contribute</h3>
<p>If you have an idea for your own spee.ch-like site on top of LBRY, fork our <a className='link--primary' href='https://github.com/lbryio/spee.ch'>github repo</a> and go to town!</p>
<p>If you want to improve spee.ch, join our <a className='link--primary' href='https://discord.gg/YjYbwhS'>discord channel</a> or solve one of our <a className='link--primary' href='https://github.com/lbryio/spee.ch/issues'>github issues</a>.</p>
</div>
</div>
</div>
</div>
);
}
};
export default AboutPage;

View file

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

View file

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

View file

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

View file

@ -1,39 +0,0 @@
import React from 'react';
import { withRouter } from 'react-router-dom';
import SEO from 'components/SEO';
import NavBar from 'containers/NavBar';
import ChannelLoginForm from 'containers/ChannelLoginForm';
import ChannelCreateForm from 'containers/ChannelCreateForm';
class LoginPage extends React.Component {
componentWillReceiveProps (newProps) {
// re-route the user to the homepage if the user is logged in
if (newProps.loggedInChannelName !== this.props.loggedInChannelName) {
this.props.history.push(`/`);
}
}
render () {
return (
<div>
<SEO pageTitle={'Login'} pageUri={'login'} />
<NavBar />
<div className='row row--padded'>
<div className='column column--5 column--med-10 align-content-top'>
<div className='column column--8 column--med-10'>
<p>Channels allow you to publish and group content under an identity. You can create a channel for yourself, or share one with like-minded friends. You can create 1 channel, or 100, so whether you're <a className='link--primary' target='_blank' href='/@catalonia2017:43dcf47163caa21d8404d9fe9b30f78ef3e146a8'>documenting important events</a>, or making a public repository for <a className='link--primary' target='_blank' href='/@catGifs'>cat gifs</a> (password: '1234'), try creating a channel for it!</p>
</div>
</div><div className='column column--5 column--med-10 align-content-top'>
<div className='column column--8 column--med-10'>
<h3 className='h3--no-bottom'>Log in to an existing channel:</h3>
<ChannelLoginForm />
<h3 className='h3--no-bottom'>Create a brand new channel:</h3>
<ChannelCreateForm />
</div>
</div>
</div>
</div>
);
}
};
export default withRouter(LoginPage);

View file

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

View file

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

View file

@ -1,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;

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -1,35 +0,0 @@
import {call, put, select, takeLatest} from 'redux-saga/effects';
import * as actions from 'constants/show_action_types';
import { updateFileAvailability, updateDisplayAssetError } from 'actions/show';
import { UNAVAILABLE, AVAILABLE } from 'constants/asset_display_states';
import { checkFileAvailability, triggerClaimGet } from 'api/fileApi';
import { selectSiteHost } from 'selectors/site';
function * retrieveFile (action) {
const name = action.data.name;
const claimId = action.data.claimId;
const host = yield select(selectSiteHost);
// see if the file is available
let isAvailable;
try {
({ data: isAvailable } = yield call(checkFileAvailability, claimId, host, name));
} catch (error) {
return yield put(updateDisplayAssetError(error.message));
};
if (isAvailable) {
yield put(updateDisplayAssetError(null));
return yield put(updateFileAvailability(AVAILABLE));
}
yield put(updateFileAvailability(UNAVAILABLE));
// initiate get request for the file
try {
yield call(triggerClaimGet, claimId, host, name);
} catch (error) {
return yield put(updateDisplayAssetError(error.message));
};
yield put(updateFileAvailability(AVAILABLE));
};
export function * watchFileIsRequested () {
yield takeLatest(actions.FILE_REQUESTED, retrieveFile);
};

View file

@ -1,17 +0,0 @@
import { all } from 'redux-saga/effects';
import { watchHandleShowPageUri } from './show_uri';
import { watchNewAssetRequest } from './show_asset';
import { watchNewChannelRequest, watchUpdateChannelClaims } from './show_channel';
import { watchFileIsRequested } from './file';
import { watchPublishStart } from './publish';
export default function * rootSaga () {
yield all([
watchHandleShowPageUri(),
watchNewAssetRequest(),
watchNewChannelRequest(),
watchUpdateChannelClaims(),
watchFileIsRequested(),
watchPublishStart(),
]);
}

View file

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

View file

@ -1,55 +0,0 @@
import { call, put, select, takeLatest } from 'redux-saga/effects';
import * as actions from 'constants/show_action_types';
import { addRequestToRequestList, onRequestError, onRequestUpdate, addAssetToAssetList } from 'actions/show';
import { getLongClaimId, getShortId, getClaimData } from 'api/assetApi';
import { selectShowState } from 'selectors/show';
import { selectSiteHost } from 'selectors/site';
export function * newAssetRequest (action) {
const { requestType, requestId, name, modifier } = action.data;
// put an action to update the request in redux
yield put(onRequestUpdate(requestType, requestId));
// is this an existing request?
// If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
const host = yield select(selectSiteHost);
if (state.requestList[requestId]) {
return null;
}
// get long id && add request to request list
let longId;
try {
({data: longId} = yield call(getLongClaimId, host, name, modifier));
} catch (error) {
return yield put(onRequestError(error.message));
}
const assetKey = `a#${name}#${longId}`;
yield put(addRequestToRequestList(requestId, null, assetKey));
// is this an existing asset?
// If this asset is in the asset list, it's already been fetched
if (state.assetList[assetKey]) {
return null;
}
// get short Id
let shortId;
try {
({data: shortId} = yield call(getShortId, host, name, longId));
} catch (error) {
return yield put(onRequestError(error.message));
}
// get asset claim data
let claimData;
try {
({data: claimData} = yield call(getClaimData, host, name, longId));
} catch (error) {
return yield put(onRequestError(error.message));
}
// add asset to asset list
yield put(addAssetToAssetList(assetKey, null, name, longId, shortId, claimData));
// clear any errors in request error
yield put(onRequestError(null));
};
export function * watchNewAssetRequest () {
yield takeLatest(actions.ASSET_REQUEST_NEW, newAssetRequest);
};

View file

@ -1,65 +0,0 @@
import {call, put, select, takeLatest} from 'redux-saga/effects';
import * as actions from 'constants/show_action_types';
import { addNewChannelToChannelList, addRequestToRequestList, onRequestError, onRequestUpdate, updateChannelClaims } from 'actions/show';
import { getChannelClaims, getChannelData } from 'api/channelApi';
import { selectShowState } from 'selectors/show';
import { selectSiteHost } from 'selectors/site';
export function * newChannelRequest (action) {
const { requestType, requestId, channelName, channelId } = action.data;
// put an action to update the request in redux
yield put(onRequestUpdate(requestType, requestId));
// is this an existing request?
// If this uri is in the request list, it's already been fetched
const state = yield select(selectShowState);
const host = yield select(selectSiteHost);
if (state.requestList[requestId]) {
return null;
}
// get channel long id
let longId, shortId;
try {
({ data: {longChannelClaimId: longId, shortChannelClaimId: shortId} } = yield call(getChannelData, host, channelName, channelId));
} catch (error) {
return yield put(onRequestError(error.message));
}
// store the request in the channel requests list
const channelKey = `c#${channelName}#${longId}`;
yield put(addRequestToRequestList(requestId, null, channelKey));
// is this an existing channel?
// If this channel is in the channel list, it's already been fetched
if (state.channelList[channelKey]) {
return null;
}
// get channel claims data
let claimsData;
try {
({ data: claimsData } = yield call(getChannelClaims, host, longId, channelName, 1));
} catch (error) {
return yield put(onRequestError(error.message));
}
// store the channel data in the channel list
yield put(addNewChannelToChannelList(channelKey, channelName, shortId, longId, claimsData));
// clear any request errors
yield put(onRequestError(null));
}
export function * watchNewChannelRequest () {
yield takeLatest(actions.CHANNEL_REQUEST_NEW, newChannelRequest);
};
function * getNewClaimsAndUpdateChannel (action) {
const { channelKey, name, longId, page } = action.data;
const host = yield select(selectSiteHost);
let claimsData;
try {
({ data: claimsData } = yield call(getChannelClaims, host, longId, name, page));
} catch (error) {
return yield put(onRequestError(error.message));
}
yield put(updateChannelClaims(channelKey, claimsData));
}
export function * watchUpdateChannelClaims () {
yield takeLatest(actions.CHANNEL_CLAIMS_UPDATE_ASYNC, getNewClaimsAndUpdateChannel);
}

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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);
};

View file

@ -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;
}
};

View file

@ -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.');
}
},
};

View file

@ -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