Merge pull request #1248 from daovist/select-thumbnail

Select thumbnail, spee.ch upload
This commit is contained in:
Sean Yesmunt 2018-06-08 00:46:54 -04:00 committed by GitHub
commit 65513cf7e7
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
13 changed files with 322 additions and 26 deletions

View file

@ -48,7 +48,7 @@
"formik": "^0.10.4", "formik": "^0.10.4",
"hast-util-sanitize": "^1.1.2", "hast-util-sanitize": "^1.1.2",
"keytar": "^4.2.1", "keytar": "^4.2.1",
"lbry-redux": "lbryio/lbry-redux#543af2fcee7e4c45ccaf73af7b47d4b1a5d8ad44", "lbry-redux": "lbryio/lbry-redux#7759bc6e8c482bed173d1f10aee6f6f9a439a15a",
"localforage": "^1.7.1", "localforage": "^1.7.1",
"mixpanel-browser": "^2.17.1", "mixpanel-browser": "^2.17.1",
"moment": "^2.22.0", "moment": "^2.22.0",

View file

@ -9,6 +9,8 @@ type Props = {
type: string, type: string,
currentPath: ?string, currentPath: ?string,
onFileChosen: (string, string) => void, onFileChosen: (string, string) => void,
fileLabel: ?string,
directoryLabel?: string,
}; };
class FileSelector extends React.PureComponent<Props> { class FileSelector extends React.PureComponent<Props> {
@ -47,15 +49,14 @@ class FileSelector extends React.PureComponent<Props> {
input: ?HTMLInputElement; input: ?HTMLInputElement;
render() { render() {
const { type, currentPath } = this.props; const { type, currentPath, fileLabel, directoryLabel } = this.props;
const label =
type === 'file' ? fileLabel || __('Choose File') : directoryLabel || __('Choose Directory');
return ( return (
<FormRow verticallyCentered padded> <FormRow verticallyCentered padded>
<Button <Button button="primary" onClick={() => this.handleButtonClick()} label={label} />
button="primary"
onClick={() => this.handleButtonClick()}
label={type === 'file' ? __('Choose File') : __('Choose Directory')}
/>
<input <input
webkitdirectory="true" webkitdirectory="true"
className="input-copyable" className="input-copyable"

View file

@ -7,6 +7,7 @@ import ChannelSection from 'component/selectChannel';
import classnames from 'classnames'; import classnames from 'classnames';
import type { PublishParams, UpdatePublishFormData } from 'redux/reducers/publish'; import type { PublishParams, UpdatePublishFormData } from 'redux/reducers/publish';
import FileSelector from 'component/common/file-selector'; import FileSelector from 'component/common/file-selector';
import SelectThumbnail from 'component/selectThumbnail';
import { COPYRIGHT, OTHER } from 'constants/licenses'; import { COPYRIGHT, OTHER } from 'constants/licenses';
import { CHANNEL_NEW, CHANNEL_ANONYMOUS, MINIMUM_PUBLISH_BID } from 'constants/claim'; import { CHANNEL_NEW, CHANNEL_ANONYMOUS, MINIMUM_PUBLISH_BID } from 'constants/claim';
import * as icons from 'constants/icons'; import * as icons from 'constants/icons';
@ -21,6 +22,8 @@ type Props = {
editingURI: ?string, editingURI: ?string,
title: ?string, title: ?string,
thumbnail: ?string, thumbnail: ?string,
uploadThumbnailStatus: ?string,
thumbnailPath: ?string,
description: ?string, description: ?string,
language: string, language: string,
nsfw: boolean, nsfw: boolean,
@ -49,7 +52,8 @@ type Props = {
clearPublish: () => void, clearPublish: () => void,
resolveUri: string => void, resolveUri: string => void,
scrollToTop: () => void, scrollToTop: () => void,
prepareEdit: ({}, string) => void, prepareEdit: ({}) => void,
resetThumbnailStatus: () => void,
}; };
class PublishForm extends React.PureComponent<Props> { class PublishForm extends React.PureComponent<Props> {
@ -67,7 +71,10 @@ class PublishForm extends React.PureComponent<Props> {
(this: any).getNewUri = this.getNewUri.bind(this); (this: any).getNewUri = this.getNewUri.bind(this);
} }
// Returns a new uri to be used in the form and begins to resolve that uri for bid help text componentWillMount() {
this.props.resetThumbnailStatus();
}
getNewUri(name: string, channel: string) { getNewUri(name: string, channel: string) {
const { resolveUri } = this.props; const { resolveUri } = this.props;
// If they are midway through a channel creation, treat it as anonymous until it completes // If they are midway through a channel creation, treat it as anonymous until it completes
@ -267,6 +274,7 @@ class PublishForm extends React.PureComponent<Props> {
editingURI, editingURI,
title, title,
thumbnail, thumbnail,
uploadThumbnailStatus,
description, description,
language, language,
nsfw, nsfw,
@ -289,6 +297,8 @@ class PublishForm extends React.PureComponent<Props> {
bidError, bidError,
publishing, publishing,
clearPublish, clearPublish,
thumbnailPath,
resetThumbnailStatus,
} = this.props; } = this.props;
const formDisabled = (!filePath && !editingURI) || publishing; const formDisabled = (!filePath && !editingURI) || publishing;
@ -349,18 +359,6 @@ class PublishForm extends React.PureComponent<Props> {
onChange={e => updatePublishForm({ title: e.target.value })} onChange={e => updatePublishForm({ title: e.target.value })}
/> />
</FormRow> </FormRow>
<FormRow padded>
<FormField
stretch
type="text"
name="content_thumbnail"
label={__('Thumbnail')}
placeholder="http://spee.ch/mylogo"
value={thumbnail}
disabled={formDisabled}
onChange={e => updatePublishForm({ thumbnail: e.target.value })}
/>
</FormRow>
<FormRow padded> <FormRow padded>
<FormField <FormField
stretch stretch
@ -375,6 +373,24 @@ class PublishForm extends React.PureComponent<Props> {
</FormRow> </FormRow>
</section> </section>
<section className="card card--section">
<div className="card__title">{__('Thumbnail')}</div>
<div className="card__subtitle">
{__(
'Upload your thumbnail to spee.ch, or enter the url manually. Learn more about spee.ch '
)}
<Button button="link" label={__('here')} href="https://spee.ch/about" />.
</div>
<SelectThumbnail
thumbnailPath={thumbnailPath}
thumbnail={thumbnail}
uploadThumbnailStatus={uploadThumbnailStatus}
updatePublishForm={updatePublishForm}
formDisabled={formDisabled}
resetThumbnailStatus={resetThumbnailStatus}
/>
</section>
<section className="card card--section"> <section className="card card--section">
<div className="card__title">{__('Price')}</div> <div className="card__title">{__('Price')}</div>
<div className="card__subtitle">{__('How much will this content cost?')}</div> <div className="card__subtitle">{__('How much will this content cost?')}</div>

View file

@ -0,0 +1,12 @@
import { connect } from 'react-redux';
import { doNotify } from 'lbry-redux';
import SelectThumbnail from './view';
const perform = dispatch => ({
openModal: (modal, props) => dispatch(doNotify(modal, props)),
});
export default connect(
null,
perform
)(SelectThumbnail);

View file

@ -0,0 +1,88 @@
// @flow
import { STATUSES, MODALS } from 'lbry-redux';
import React from 'react';
import { FormField, FormRow } from 'component/common/form';
import FileSelector from 'component/common/file-selector';
import Button from 'component/button';
type Props = {
thumbnail: ?string,
formDisabled: boolean,
uploadThumbnailStatus: string,
thumbnailPath: ?string,
openModal: ({ id: string }, {}) => void,
updatePublishForm: ({}) => void,
resetThumbnailStatus: () => void,
};
class SelectThumbnail extends React.PureComponent<Props> {
render() {
const {
thumbnail,
formDisabled,
uploadThumbnailStatus: status,
openModal,
updatePublishForm,
thumbnailPath,
resetThumbnailStatus,
} = this.props;
return (
<div>
{status === STATUSES.API_DOWN || status === STATUSES.MANUAL ? (
<FormRow padded>
<FormField
stretch
type="text"
name="content_thumbnail"
label={__('Url')}
placeholder="http://spee.ch/mylogo"
value={thumbnail}
disabled={formDisabled}
onChange={e => updatePublishForm({ thumbnail: e.target.value })}
/>
</FormRow>
) : (
<div className="form-row--padded">
{(status === STATUSES.READY || status === STATUSES.COMPLETE) && (
<FileSelector
currentPath={thumbnailPath}
fileLabel={__('Choose Thumbnail')}
onFileChosen={path => openModal({ id: MODALS.CONFIRM_THUMBNAIL_UPLOAD }, { path })}
/>
)}
{status === STATUSES.COMPLETE && (
<div>
<p>
Upload complete. View it{' '}
<Button button="link" href={thumbnail} label={__('here')} />.
</p>
<Button button="link" label={__('New thumbnail')} onClick={resetThumbnailStatus} />
</div>
)}
</div>
)}
<div className="card__actions">
{status === STATUSES.READY && (
<Button
button="link"
label={__('Or enter a URL manually')}
onClick={() => updatePublishForm({ uploadThumbnailStatus: STATUSES.MANUAL })}
/>
)}
{status === STATUSES.MANUAL && (
<Button
button="link"
label={__('Use thumbnail upload tool')}
onClick={() => updatePublishForm({ uploadThumbnailStatus: STATUSES.READY })}
/>
)}
</div>
{status === STATUSES.IN_PROGRESS && <p>{__('Uploading thumbnail')}...</p>}
</div>
);
}
}
export default SelectThumbnail;

View file

@ -0,0 +1,5 @@
export const API_DOWN = 'apiDown';
export const READY = 'ready';
export const IN_PROGRESS = 'inProgress';
export const COMPLETE = 'complete';
export const MANUAL = 'manual';

View file

@ -0,0 +1,21 @@
import { connect } from 'react-redux';
import { doHideNotification } from 'lbry-redux';
import { doUploadThumbnail, doUpdatePublishForm } from 'redux/actions/publish';
import { selectPublishFormValues } from 'redux/selectors/publish';
import ModalConfirmThumbnailUpload from './view';
const select = state => {
const publishState = selectPublishFormValues(state);
return { nsfw: publishState.nsfw };
};
const perform = dispatch => ({
closeModal: () => dispatch(doHideNotification()),
upload: (path, nsfw = false) => dispatch(doUploadThumbnail(path, nsfw)),
updatePublishForm: value => dispatch(doUpdatePublishForm(value)),
});
export default connect(
select,
perform
)(ModalConfirmThumbnailUpload);

View file

@ -0,0 +1,48 @@
// @flow
import React from 'react';
import { Modal } from 'modal/modal';
import { FormField } from 'component/common/form';
type Props = {
upload: (string, boolean) => void,
path: string,
nsfw: boolean,
closeModal: () => void,
updatePublishForm: ({}) => void,
};
class ModalConfirmThumbnailUpload extends React.PureComponent<Props> {
upload() {
const { upload, updatePublishForm, closeModal, path, nsfw } = this.props;
upload(path, nsfw);
updatePublishForm({ thumbnailPath: path });
closeModal();
}
render() {
const { closeModal, path, updatePublishForm, nsfw } = this.props;
return (
<Modal
isOpen
contentLabel={__('Confirm Thumbnail Upload')}
type="confirm"
confirmButtonLabel={__('Upload')}
onConfirmed={() => this.upload()}
onAborted={closeModal}
>
<p>{__('Are you sure you want to upload this thumbnail to spee.ch')}?</p>
<blockquote>{path}</blockquote>
<FormField
type="checkbox"
name="content_is_mature"
postfix={__('Mature audiences only')}
checked={nsfw}
onChange={event => updatePublishForm({ nsfw: event.target.checked })}
/>
</Modal>
);
}
}
export default ModalConfirmThumbnailUpload;

View file

@ -22,8 +22,13 @@ import ModalConfirmTransaction from 'modal/modalConfirmTransaction';
import ModalSendTip from '../modalSendTip'; import ModalSendTip from '../modalSendTip';
import ModalPublish from '../modalPublish'; import ModalPublish from '../modalPublish';
import ModalOpenExternalLink from '../modalOpenExternalLink'; import ModalOpenExternalLink from '../modalOpenExternalLink';
import ModalConfirmThumbnailUpload from 'modal/modalConfirmThumbnailUpload';
class ModalRouter extends React.PureComponent { type Props = {
modal: string,
};
class ModalRouter extends React.PureComponent<Props> {
constructor(props) { constructor(props) {
super(props); super(props);
@ -56,7 +61,7 @@ class ModalRouter extends React.PureComponent {
if ( if (
transitionModal && transitionModal &&
(transitionModal != this.state.lastTransitionModal || page != this.state.lastTransitionPage) (transitionModal !== this.state.lastTransitionModal || page !== this.state.lastTransitionPage)
) { ) {
openModal({ id: transitionModal }); openModal({ id: transitionModal });
this.setState({ this.setState({
@ -158,6 +163,8 @@ class ModalRouter extends React.PureComponent {
return <ModalOpenExternalLink {...notificationProps} />; return <ModalOpenExternalLink {...notificationProps} />;
case MODALS.CONFIRM_TRANSACTION: case MODALS.CONFIRM_TRANSACTION:
return <ModalConfirmTransaction {...notificationProps} />; return <ModalConfirmTransaction {...notificationProps} />;
case MODALS.CONFIRM_THUMBNAIL_UPLOAD:
return <ModalConfirmThumbnailUpload {...notificationProps} />;
default: default:
return null; return null;
} }

View file

@ -10,6 +10,7 @@ import {
import { doNavigate } from 'redux/actions/navigation'; import { doNavigate } from 'redux/actions/navigation';
import { selectPublishFormValues } from 'redux/selectors/publish'; import { selectPublishFormValues } from 'redux/selectors/publish';
import { import {
doResetThumbnailStatus,
doClearPublish, doClearPublish,
doUpdatePublishForm, doUpdatePublishForm,
doPublish, doPublish,
@ -55,7 +56,8 @@ const perform = dispatch => ({
resolveUri: uri => dispatch(doResolveUri(uri)), resolveUri: uri => dispatch(doResolveUri(uri)),
publish: params => dispatch(doPublish(params)), publish: params => dispatch(doPublish(params)),
navigate: path => dispatch(doNavigate(path)), navigate: path => dispatch(doNavigate(path)),
prepareEdit: (claim, uri) => dispatch(doPrepareEdit(claim, uri)), prepareEdit: claim => dispatch(doPrepareEdit(claim)),
resetThumbnailStatus: () => dispatch(doResetThumbnailStatus()),
}); });
export default connect(select, perform)(PublishPage); export default connect(select, perform)(PublishPage);

View file

@ -6,6 +6,8 @@ import {
doNotify, doNotify,
MODALS, MODALS,
selectMyChannelClaims, selectMyChannelClaims,
STATUSES,
batchActions,
} from 'lbry-redux'; } from 'lbry-redux';
import { selectPendingPublishes } from 'redux/selectors/publish'; import { selectPendingPublishes } from 'redux/selectors/publish';
import type { import type {
@ -13,6 +15,8 @@ import type {
UpdatePublishFormAction, UpdatePublishFormAction,
PublishParams, PublishParams,
} from 'redux/reducers/publish'; } from 'redux/reducers/publish';
import fs from 'fs';
import path from 'path';
type Action = UpdatePublishFormAction | { type: ACTIONS.CLEAR_PUBLISH }; type Action = UpdatePublishFormAction | { type: ACTIONS.CLEAR_PUBLISH };
type PromiseAction = Promise<Action>; type PromiseAction = Promise<Action>;
@ -30,6 +34,90 @@ export const doUpdatePublishForm = (publishFormValue: UpdatePublishFormData) =>
data: { ...publishFormValue }, data: { ...publishFormValue },
}); });
export const doResetThumbnailStatus = () => (dispatch: Dispatch): PromiseAction => {
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
thumbnailPath: '',
},
});
return fetch('https://spee.ch/api/channel/availability/@testing')
.then(() =>
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
uploadThumbnailStatus: STATUSES.READY,
thumbnail: '',
nsfw: false,
},
})
)
.catch(() =>
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
uploadThumbnailStatus: STATUSES.API_DOWN,
thumbnail: '',
nsfw: false,
},
})
);
};
export const doUploadThumbnail = (filePath: string, nsfw: boolean) => (dispatch: Dispatch) => {
const thumbnail = fs.readFileSync(filePath);
const fileExt = path.extname(filePath);
const makeid = () => {
let text = '';
const possible = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789';
for (let i = 0; i < 24; i += 1) text += possible.charAt(Math.floor(Math.random() * 62));
return text;
};
const uploadError = (error = '') =>
dispatch(
batchActions(
{
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: { uploadThumbnailStatus: STATUSES.API_DOWN },
},
dispatch(doNotify({ id: MODALS.ERROR, error }))
)
);
dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: { uploadThumbnailStatus: STATUSES.IN_PROGRESS },
});
const data = new FormData();
const name = makeid();
const blob = new Blob([thumbnail], { type: `image/${fileExt.slice(1)}` });
data.append('name', name);
data.append('file', blob);
data.append('nsfw', nsfw.toString());
return fetch('https://spee.ch/api/claim/publish', {
method: 'POST',
body: data,
})
.then(response => response.json())
.then(
json =>
json.success
? dispatch({
type: ACTIONS.UPDATE_PUBLISH_FORM,
data: {
uploadThumbnailStatus: STATUSES.COMPLETE,
thumbnail: `${json.data.url}${fileExt}`,
},
})
: uploadError('Upload failed')
)
.catch(err => uploadError(err.message));
};
export const doPrepareEdit = (claim: any, uri: string) => (dispatch: Dispatch) => { export const doPrepareEdit = (claim: any, uri: string) => (dispatch: Dispatch) => {
const { const {
name, name,
@ -39,6 +127,7 @@ export const doPrepareEdit = (claim: any, uri: string) => (dispatch: Dispatch) =
stream: { metadata }, stream: { metadata },
}, },
} = claim; } = claim;
const { const {
author, author,
description, description,

View file

@ -2,6 +2,7 @@
import { handleActions } from 'util/redux-utils'; import { handleActions } from 'util/redux-utils';
import { buildURI } from 'lbry-redux'; import { buildURI } from 'lbry-redux';
import * as ACTIONS from 'constants/action_types'; import * as ACTIONS from 'constants/action_types';
import * as STATUSES from 'constants/thumbnail_upload_statuses';
import { CHANNEL_ANONYMOUS } from 'constants/claim'; import { CHANNEL_ANONYMOUS } from 'constants/claim';
type PublishState = { type PublishState = {
@ -14,6 +15,8 @@ type PublishState = {
}, },
title: string, title: string,
thumbnail: string, thumbnail: string,
thumbnailPath: string,
uploadThumbnailStatus: string,
description: string, description: string,
language: string, language: string,
tosAccepted: boolean, tosAccepted: boolean,
@ -38,6 +41,8 @@ export type UpdatePublishFormData = {
}, },
title?: string, title?: string,
thumbnail?: string, thumbnail?: string,
uploadThumbnailStatus?: string,
thumbnailPath?: string,
description?: string, description?: string,
language?: string, language?: string,
tosAccepted?: boolean, tosAccepted?: boolean,
@ -96,6 +101,8 @@ const defaultState: PublishState = {
}, },
title: '', title: '',
thumbnail: '', thumbnail: '',
thumbnailPath: '',
uploadThumbnailStatus: STATUSES.API_DOWN,
description: '', description: '',
language: 'en', language: 'en',
nsfw: false, nsfw: false,

View file

@ -5647,9 +5647,9 @@ lazy-val@^1.0.3:
version "1.0.3" version "1.0.3"
resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc" resolved "https://registry.yarnpkg.com/lazy-val/-/lazy-val-1.0.3.tgz#bb97b200ef00801d94c317e29dc6ed39e31c5edc"
lbry-redux@lbryio/lbry-redux#543af2fcee7e4c45ccaf73af7b47d4b1a5d8ad44: lbry-redux@lbryio/lbry-redux#7759bc6e8c482bed173d1f10aee6f6f9a439a15a:
version "0.0.1" version "0.0.1"
resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/543af2fcee7e4c45ccaf73af7b47d4b1a5d8ad44" resolved "https://codeload.github.com/lbryio/lbry-redux/tar.gz/7759bc6e8c482bed173d1f10aee6f6f9a439a15a"
dependencies: dependencies:
proxy-polyfill "0.1.6" proxy-polyfill "0.1.6"
reselect "^3.0.0" reselect "^3.0.0"