Thumbnail upload fixes (#6860)
more improvements, fix url, do the same for cover remember url, error if invalid unneeded addition Fix delayed message Lint Allow empty values (placeholder and Gerbil) Fix filepath crash Fix button
This commit is contained in:
parent
ef5701bb38
commit
b256a4396b
9 changed files with 97 additions and 109 deletions
|
@ -22,6 +22,8 @@ import SUPPORTED_LANGUAGES from 'constants/supported_languages';
|
|||
import WalletSpendableBalanceHelp from 'component/walletSpendableBalanceHelp';
|
||||
import { SIMPLE_SITE } from 'config';
|
||||
import { sortLanguageMap } from 'util/default-languages';
|
||||
import ThumbnailBrokenImage from 'component/selectThumbnail/thumbnail-broken.png';
|
||||
import Gerbil from 'component/channelThumbnail/gerbil.png';
|
||||
|
||||
const LANG_NONE = 'none';
|
||||
|
||||
|
@ -52,7 +54,7 @@ type Props = {
|
|||
onDone: () => void,
|
||||
openModal: (
|
||||
id: string,
|
||||
{ onUpdate: (string) => void, assetName: string, helpText: string, currentValue: string, title: string }
|
||||
{ onUpdate: (string, boolean) => void, assetName: string, helpText: string, currentValue: string, title: string }
|
||||
) => void,
|
||||
uri: string,
|
||||
disabled: boolean,
|
||||
|
@ -90,7 +92,9 @@ function ChannelForm(props: Props) {
|
|||
} = props;
|
||||
const [nameError, setNameError] = React.useState(undefined);
|
||||
const [bidError, setBidError] = React.useState('');
|
||||
const [isUpload, setIsUpload] = React.useState({ cover: false, thumbnail: false });
|
||||
const [coverError, setCoverError] = React.useState(false);
|
||||
const [thumbError, setThumbError] = React.useState(false);
|
||||
const { claim_id: claimId } = claim || {};
|
||||
const [params, setParams]: [any, (any) => void] = React.useState(getChannelParams());
|
||||
const { channelName } = parseURI(uri);
|
||||
|
@ -112,10 +116,12 @@ function ChannelForm(props: Props) {
|
|||
creatingChannel ||
|
||||
updatingChannel ||
|
||||
nameError ||
|
||||
thumbError ||
|
||||
coverError ||
|
||||
bidError ||
|
||||
(isNewChannel && !params.name)
|
||||
);
|
||||
}, [isClaimingInitialRewards, creatingChannel, updatingChannel, nameError, bidError, isNewChannel, params]);
|
||||
}, [isClaimingInitialRewards, creatingChannel, updatingChannel, nameError, thumbError, coverError, bidError, isNewChannel, params.name]);
|
||||
|
||||
function getChannelParams() {
|
||||
// fill this in with sdk data
|
||||
|
@ -195,12 +201,16 @@ function ChannelForm(props: Props) {
|
|||
setParams({ ...params, languages: langs });
|
||||
}
|
||||
|
||||
function handleThumbnailChange(thumbnailUrl: string) {
|
||||
function handleThumbnailChange(thumbnailUrl: string, uploadSelected: boolean) {
|
||||
setParams({ ...params, thumbnailUrl });
|
||||
setIsUpload({ ...isUpload, thumbnail: uploadSelected });
|
||||
setThumbError(false);
|
||||
}
|
||||
|
||||
function handleCoverChange(coverUrl: string) {
|
||||
function handleCoverChange(coverUrl: string, uploadSelected: boolean) {
|
||||
setParams({ ...params, coverUrl });
|
||||
setIsUpload({ ...isUpload, cover: uploadSelected });
|
||||
setCoverError(false);
|
||||
}
|
||||
|
||||
function handleSubmit() {
|
||||
|
@ -225,6 +235,9 @@ function ChannelForm(props: Props) {
|
|||
if (errorMsg && errorMsg.includes(LIMIT_ERR_PARTIAL_MSG)) {
|
||||
errorMsg = __('Transaction limit reached. Try reducing the Description length.');
|
||||
}
|
||||
if ((!isUpload.thumbnail && thumbError) || (!isUpload.cover && coverError)) {
|
||||
errorMsg = __('Invalid %error_type%', { error_type: (thumbError && 'thumbnail') || (coverError && 'cover image') });
|
||||
}
|
||||
|
||||
React.useEffect(() => {
|
||||
let nameError;
|
||||
|
@ -247,6 +260,17 @@ function ChannelForm(props: Props) {
|
|||
}
|
||||
}, [hasClaimedInitialRewards, claimInitialRewards]);
|
||||
|
||||
const coverSrc = coverError ? ThumbnailBrokenImage : params.coverUrl;
|
||||
|
||||
let thumbnailPreview;
|
||||
if (!params.thumbnailUrl) {
|
||||
thumbnailPreview = Gerbil;
|
||||
} else if (thumbError) {
|
||||
thumbnailPreview = ThumbnailBrokenImage;
|
||||
} else {
|
||||
thumbnailPreview = params.thumbnailUrl;
|
||||
}
|
||||
|
||||
// TODO clear and bail after submit
|
||||
return (
|
||||
<>
|
||||
|
@ -258,7 +282,7 @@ function ChannelForm(props: Props) {
|
|||
title={__('Cover')}
|
||||
onClick={() =>
|
||||
openModal(MODALS.IMAGE_UPLOAD, {
|
||||
onUpdate: (coverUrl) => handleCoverChange(coverUrl),
|
||||
onUpdate: (coverUrl, isUpload) => handleCoverChange(coverUrl, isUpload),
|
||||
title: __('Edit Cover Image'),
|
||||
helpText: __('(6.25:1)'),
|
||||
assetName: __('Cover Image'),
|
||||
|
@ -270,11 +294,16 @@ function ChannelForm(props: Props) {
|
|||
/>
|
||||
</div>
|
||||
{params.coverUrl &&
|
||||
(coverError ? (
|
||||
(coverError && isUpload.cover ? (
|
||||
<div className="channel-cover__custom--waiting">{__('This will be visible in a few minutes.')}</div>
|
||||
) : (
|
||||
<img className="channel-cover__custom" src={params.coverUrl} onError={() => setCoverError(true)} />
|
||||
))}
|
||||
<img
|
||||
className="channel-cover__custom"
|
||||
src={coverSrc}
|
||||
onError={() => setCoverError(true)}
|
||||
/>
|
||||
)
|
||||
)}
|
||||
<div className="channel__primary-info">
|
||||
<div className="channel__edit-thumb">
|
||||
<Button
|
||||
|
@ -282,7 +311,7 @@ function ChannelForm(props: Props) {
|
|||
title={__('Edit')}
|
||||
onClick={() =>
|
||||
openModal(MODALS.IMAGE_UPLOAD, {
|
||||
onUpdate: (v) => handleThumbnailChange(v),
|
||||
onUpdate: (thumbnailUrl, isUpload) => handleThumbnailChange(thumbnailUrl, isUpload),
|
||||
title: __('Edit Thumbnail Image'),
|
||||
helpText: __('(1:1)'),
|
||||
assetName: __('Thumbnail'),
|
||||
|
@ -296,9 +325,11 @@ function ChannelForm(props: Props) {
|
|||
<ChannelThumbnail
|
||||
className="channel__thumbnail--channel-page"
|
||||
uri={uri}
|
||||
thumbnailPreview={params.thumbnailUrl}
|
||||
thumbnailPreview={thumbnailPreview}
|
||||
allowGifs
|
||||
showDelayedMessage
|
||||
showDelayedMessage={isUpload.thumbnail}
|
||||
setThumbError={(v) => setThumbError(v)}
|
||||
thumbError={thumbError}
|
||||
/>
|
||||
<h1 className="channel__title">
|
||||
{params.title || (channelName && '@' + channelName) || (params.name && '@' + params.name)}
|
||||
|
|
|
@ -23,6 +23,10 @@ type Props = {
|
|||
showDelayedMessage?: boolean,
|
||||
noLazyLoad?: boolean,
|
||||
hideStakedIndicator?: boolean,
|
||||
xsmall?: boolean,
|
||||
noOptimization?: boolean,
|
||||
setThumbError: (boolean) => void,
|
||||
thumbError: boolean,
|
||||
};
|
||||
|
||||
function ChannelThumbnail(props: Props) {
|
||||
|
@ -41,15 +45,15 @@ function ChannelThumbnail(props: Props) {
|
|||
showDelayedMessage = false,
|
||||
noLazyLoad,
|
||||
hideStakedIndicator = false,
|
||||
setThumbError,
|
||||
} = props;
|
||||
const [thumbError, setThumbError] = React.useState(false);
|
||||
const shouldResolve = claim === undefined;
|
||||
const thumbnail = rawThumbnail && rawThumbnail.trim().replace(/^http:\/\//i, 'https://');
|
||||
const thumbnailPreview = rawThumbnailPreview && rawThumbnailPreview.trim().replace(/^http:\/\//i, 'https://');
|
||||
const channelThumbnail = thumbnail || thumbnailPreview;
|
||||
const defaultAvater = AVATAR_DEFAULT || Gerbil;
|
||||
const channelThumbnail = thumbnailPreview || thumbnail || defaultAvater;
|
||||
const isGif = channelThumbnail && channelThumbnail.endsWith('gif');
|
||||
const showThumb = (!obscure && !!thumbnail) || thumbnailPreview;
|
||||
const defaultAvater = AVATAR_DEFAULT || Gerbil;
|
||||
|
||||
// Generate a random color class based on the first letter of the channel name
|
||||
const { channelName } = parseURI(uri);
|
||||
|
@ -75,6 +79,7 @@ function ChannelThumbnail(props: Props) {
|
|||
</FreezeframeWrapper>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div
|
||||
className={classnames('channel-thumbnail', className, {
|
||||
|
@ -84,30 +89,17 @@ function ChannelThumbnail(props: Props) {
|
|||
'channel-thumbnail--resolving': isResolving,
|
||||
})}
|
||||
>
|
||||
{!showThumb && (
|
||||
{showDelayedMessage ? (
|
||||
<div className="channel-thumbnail--waiting">{__('This will be visible in a few minutes.')}</div>
|
||||
) : (
|
||||
<OptimizedImage
|
||||
alt={__('Channel profile picture')}
|
||||
className="channel-thumbnail__default"
|
||||
src={!thumbError && channelThumbnail ? channelThumbnail : defaultAvater}
|
||||
className={!channelThumbnail ? 'channel-thumbnail__default' : 'channel-thumbnail__custom'}
|
||||
src={channelThumbnail}
|
||||
loading={noLazyLoad ? undefined : 'lazy'}
|
||||
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
|
||||
onError={() => setThumbError(true)}
|
||||
/>
|
||||
)}
|
||||
{showThumb && (
|
||||
<>
|
||||
{showDelayedMessage && thumbError ? (
|
||||
<div className="chanel-thumbnail--waiting">{__('This will be visible in a few minutes.')}</div>
|
||||
) : (
|
||||
<OptimizedImage
|
||||
alt={__('Channel profile picture')}
|
||||
className="channel-thumbnail__custom"
|
||||
src={!thumbError && channelThumbnail ? channelThumbnail : defaultAvater}
|
||||
loading={noLazyLoad ? undefined : 'lazy'}
|
||||
onError={() => setThumbError(true)} // if thumb fails (including due to https replace, show gerbil.
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
{!hideStakedIndicator && <ChannelStakedIndicator uri={uri} claim={claim} />}
|
||||
</div>
|
||||
);
|
||||
|
|
|
@ -7,7 +7,7 @@ import { FormField } from 'component/common/form';
|
|||
type Props = {
|
||||
type: string,
|
||||
currentPath?: ?string,
|
||||
onFileChosen: WebFile => void,
|
||||
onFileChosen: (WebFile) => void,
|
||||
label?: string,
|
||||
placeholder?: string,
|
||||
accept?: string,
|
||||
|
@ -19,17 +19,6 @@ type Props = {
|
|||
class FileSelector extends React.PureComponent<Props> {
|
||||
static defaultProps = {
|
||||
autoFocus: false,
|
||||
};
|
||||
|
||||
componentDidUpdate(prevProps: Props) {
|
||||
// If the form has just been cleared,
|
||||
// clear the file input
|
||||
if (prevProps.currentPath && !this.props.currentPath) {
|
||||
this.fileInput.current.value = null;
|
||||
}
|
||||
}
|
||||
|
||||
static defaultProps = {
|
||||
type: 'file',
|
||||
};
|
||||
|
||||
|
@ -39,6 +28,7 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
super();
|
||||
this.fileInput = React.createRef();
|
||||
this.handleFileInputSelection = this.handleFileInputSelection.bind(this);
|
||||
this.handleDirectoryInputSelection = this.handleDirectoryInputSelection.bind(this);
|
||||
this.fileInputButton = this.fileInputButton.bind(this);
|
||||
}
|
||||
|
||||
|
@ -53,6 +43,7 @@ class FileSelector extends React.PureComponent<Props> {
|
|||
if (this.props.onFileChosen) {
|
||||
this.props.onFileChosen(file);
|
||||
}
|
||||
this.fileInput.current.value = null; // clear the file input
|
||||
};
|
||||
|
||||
handleDirectoryInputSelection = () => {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
// @flow
|
||||
|
||||
import React from 'react';
|
||||
import FileSelector from 'component/common/file-selector';
|
||||
import { SPEECH_URLS } from 'lbry-redux';
|
||||
|
@ -8,7 +7,6 @@ import Button from 'component/button';
|
|||
import Card from 'component/common/card';
|
||||
import { generateThumbnailName } from 'util/generate-thumbnail-name';
|
||||
import usePersistedState from 'effects/use-persisted-state';
|
||||
import classnames from 'classnames';
|
||||
|
||||
const accept = '.png, .jpg, .jpeg, .gif';
|
||||
const SPEECH_READY = 'READY';
|
||||
|
@ -17,7 +15,7 @@ const SPEECH_UPLOADING = 'UPLOADING';
|
|||
type Props = {
|
||||
assetName: string,
|
||||
currentValue: ?string,
|
||||
onUpdate: (string) => void,
|
||||
onUpdate: (string, boolean) => void,
|
||||
recommended: string,
|
||||
title: string,
|
||||
onDone?: () => void,
|
||||
|
@ -25,27 +23,14 @@ type Props = {
|
|||
};
|
||||
|
||||
function SelectAsset(props: Props) {
|
||||
const { onUpdate, onDone, assetName, recommended, title, inline } = props;
|
||||
const { onUpdate, onDone, assetName, currentValue, recommended, title, inline } = props;
|
||||
const [pathSelected, setPathSelected] = React.useState('');
|
||||
const [fileSelected, setFileSelected] = React.useState<any>(null);
|
||||
const [uploadStatus, setUploadStatus] = React.useState(SPEECH_READY);
|
||||
const [useUrl, setUseUrl] = usePersistedState('thumbnail-upload:mode', false);
|
||||
const [url, setUrl] = React.useState('');
|
||||
const [url, setUrl] = React.useState(currentValue);
|
||||
const [error, setError] = React.useState();
|
||||
|
||||
React.useEffect(() => {
|
||||
if (pathSelected && fileSelected) {
|
||||
doUploadAsset();
|
||||
}
|
||||
}, [pathSelected, fileSelected]);
|
||||
|
||||
function handleToggleMode(useUrl) {
|
||||
setPathSelected('');
|
||||
setFileSelected(null);
|
||||
setUrl('');
|
||||
setUseUrl(useUrl);
|
||||
}
|
||||
|
||||
function doUploadAsset() {
|
||||
const uploadError = (error = '') => {
|
||||
setError(error);
|
||||
|
@ -53,7 +38,7 @@ function SelectAsset(props: Props) {
|
|||
|
||||
const onSuccess = (thumbnailUrl) => {
|
||||
setUploadStatus(SPEECH_READY);
|
||||
onUpdate(thumbnailUrl);
|
||||
onUpdate(thumbnailUrl, !useUrl);
|
||||
|
||||
if (onDone) {
|
||||
onDone();
|
||||
|
@ -93,30 +78,6 @@ function SelectAsset(props: Props) {
|
|||
}
|
||||
const formBody = (
|
||||
<>
|
||||
<div className={'section__header--actions'}>
|
||||
<div>
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': useUrl, // disable on upload status
|
||||
})}
|
||||
label={__('URL')}
|
||||
onClick={() => {
|
||||
handleToggleMode(true);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
button="alt"
|
||||
className={classnames('button-toggle', {
|
||||
'button-toggle--active': !useUrl, // disable on upload status
|
||||
})}
|
||||
label={__('Upload')}
|
||||
onClick={() => {
|
||||
handleToggleMode(false);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<fieldset-section>
|
||||
{error && <div className="error__text">{error}</div>}
|
||||
{useUrl ? (
|
||||
|
@ -129,7 +90,7 @@ function SelectAsset(props: Props) {
|
|||
value={url}
|
||||
onChange={(e) => {
|
||||
setUrl(e.target.value);
|
||||
onUpdate(e.target.value);
|
||||
onUpdate(e.target.value, !useUrl);
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
|
@ -151,6 +112,25 @@ function SelectAsset(props: Props) {
|
|||
/>
|
||||
)}
|
||||
</fieldset-section>
|
||||
|
||||
<div className="section__actions">
|
||||
{onDone && (
|
||||
<Button
|
||||
button="primary"
|
||||
type="submit"
|
||||
label={__('Done')}
|
||||
disabled={!useUrl && ((uploadStatus === SPEECH_UPLOADING) || !pathSelected || !fileSelected)}
|
||||
onClick={() => doUploadAsset()}
|
||||
/>
|
||||
)}
|
||||
<FormField
|
||||
name="toggle-upload"
|
||||
type="checkbox"
|
||||
label={__('Use a URL')}
|
||||
checked={useUrl}
|
||||
onChange={() => setUseUrl(!useUrl)}
|
||||
/>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
|
||||
|
|
|
@ -93,9 +93,7 @@ class SelectThumbnail extends React.PureComponent<Props> {
|
|||
style={{ display: 'none' }}
|
||||
src={thumbnailSrc}
|
||||
alt={__('Thumbnail Preview')}
|
||||
onError={(e) => {
|
||||
updatePublishForm({ thumbnailError: true });
|
||||
}}
|
||||
onError={() => updatePublishForm({ thumbnailError: true })}
|
||||
/>
|
||||
</div>
|
||||
<div className="column__item">
|
||||
|
@ -130,12 +128,7 @@ class SelectThumbnail extends React.PureComponent<Props> {
|
|||
)}
|
||||
{status === THUMBNAIL_STATUSES.COMPLETE && thumbnail && (
|
||||
<div className="column column--space-between">
|
||||
<div
|
||||
className="column__item thumbnail-preview"
|
||||
// style={{ backgroundImage: `url(${thumbnail})` }}
|
||||
>
|
||||
{__('This will be visible in a few minutes.')}
|
||||
</div>
|
||||
<div className="column__item thumbnail-preview" style={{ backgroundImage: `url(${thumbnail})` }} />
|
||||
<div className="column__item">
|
||||
<p>{__('Upload complete.')}</p>
|
||||
<div className="section__actions">
|
||||
|
|
|
@ -13,13 +13,16 @@ type Props = {
|
|||
class ModalConfirmThumbnailUpload extends React.PureComponent<Props> {
|
||||
upload() {
|
||||
const { upload, updatePublishForm, closeModal, file } = this.props;
|
||||
upload(file);
|
||||
updatePublishForm({ thumbnailPath: file.path });
|
||||
closeModal();
|
||||
if (file) {
|
||||
upload(file);
|
||||
updatePublishForm({ thumbnailPath: file.path });
|
||||
closeModal();
|
||||
}
|
||||
}
|
||||
|
||||
render() {
|
||||
const { closeModal, file } = this.props;
|
||||
const filePath = file && (file.path || file.name);
|
||||
|
||||
return (
|
||||
<Modal
|
||||
|
@ -33,7 +36,7 @@ class ModalConfirmThumbnailUpload extends React.PureComponent<Props> {
|
|||
>
|
||||
<label>{__('Are you sure you want to upload this thumbnail to %domain%', { domain: DOMAIN })}?</label>
|
||||
|
||||
<blockquote>{file.path || file.name}</blockquote>
|
||||
<blockquote>{filePath}</blockquote>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -2,10 +2,8 @@ import { connect } from 'react-redux';
|
|||
import { doHideModal } from 'redux/actions/app';
|
||||
import ModalImageUpload from './view';
|
||||
|
||||
const perform = dispatch => () => ({
|
||||
closeModal: () => {
|
||||
dispatch(doHideModal());
|
||||
},
|
||||
const perform = dispatch => ({
|
||||
closeModal: () => dispatch(doHideModal()),
|
||||
});
|
||||
|
||||
export default connect(null, perform)(ModalImageUpload);
|
||||
|
|
|
@ -8,7 +8,7 @@ type Props = {
|
|||
currentValue: string,
|
||||
title: string,
|
||||
helpText: string,
|
||||
onUpdate: string => void,
|
||||
onUpdate: (string, boolean) => void,
|
||||
assetName: string,
|
||||
};
|
||||
|
||||
|
@ -18,7 +18,7 @@ function ModalImageUpload(props: Props) {
|
|||
return (
|
||||
<Modal isOpen type="card" onAborted={closeModal} contentLabel={title}>
|
||||
<SelectAsset
|
||||
onUpdate={v => onUpdate(v)}
|
||||
onUpdate={(a, b) => onUpdate(a, b)}
|
||||
currentValue={currentValue}
|
||||
assetName={assetName}
|
||||
recommended={helpText}
|
||||
|
|
|
@ -84,7 +84,7 @@ $actions-z-index: 2;
|
|||
margin-right: var(--spacing-xs);
|
||||
}
|
||||
|
||||
.chanel-thumbnail--waiting {
|
||||
.channel-thumbnail--waiting {
|
||||
background-color: var(--color-gray-5);
|
||||
border-radius: var(--border-radius);
|
||||
padding-top: 4rem;
|
||||
|
|
Loading…
Add table
Reference in a new issue