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:
saltrafael 2021-08-24 21:28:23 -03:00 committed by GitHub
parent ef5701bb38
commit b256a4396b
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
9 changed files with 97 additions and 109 deletions

View file

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

View file

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

View file

@ -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 = () => {

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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