Thumbnail fixes #6860
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