Merge pull request #392 from sharetribe/fix-bug-avatar-upload

Fix bug avatar upload
This commit is contained in:
Vesa Luusua 2017-09-12 13:45:38 +03:00 committed by GitHub
commit e0c145653b
4 changed files with 187 additions and 134 deletions

View file

@ -223,6 +223,13 @@
padding-bottom: 100%;
}
.avatarInvisible {
visibility: hidden;
position: absolute;
top: -1000px;
left: -1000px;
}
.tip {
@apply --marketplaceDefaultFontStyles;
color: var(--matterColorAnti);

View file

@ -1,4 +1,4 @@
import React, { PropTypes } from 'react';
import React, { Component, PropTypes } from 'react';
import { compose } from 'redux';
import { FormattedMessage, injectIntl, intlShape } from 'react-intl';
import { Field, reduxForm, propTypes as formPropTypes } from 'redux-form';
@ -10,6 +10,7 @@ import { Avatar, Button, ImageFromFile, SpinnerIcon, TextInputField } from '../.
import css from './ProfileSettingsForm.css';
const ACCEPT_IMAGES = 'image/*';
const UPLOAD_CHANGE_DELAY = 2000; // Show spinner so that browser has time to load img srcset
const RenderAvatar = props => {
const { accept, id, input, label, type, disabled, uploadImageError } = props;
@ -58,149 +59,190 @@ RenderAvatar.propTypes = {
uploadImageError: instanceOf(Error),
};
const ProfileSettingsFormComponent = props => {
const {
className,
currentUser,
form,
handleSubmit,
intl,
invalid,
onImageUpload,
pristine,
profileImage,
rootClassName,
submitting,
updateInProgress,
updateProfileError,
uploadImageError,
uploadInProgress,
} = props;
class ProfileSettingsFormComponent extends Component {
constructor(props) {
super(props);
const user = ensureCurrentUser(currentUser);
this.uploadDelayTimeoutId = null;
this.state = { uploadDelay: false };
}
// First name
const firstNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameLabel',
});
const firstNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.firstNamePlaceholder',
});
const firstNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameRequired',
});
const firstNameRequired = validators.required(firstNameRequiredMessage);
componentWillReceiveProps(nextProps) {
// Upload delay is additional time window where Avatar is added to the DOM,
// but not yet visible (time to load image URL from srcset)
if (this.props.uploadInProgress && !nextProps.uploadInProgress) {
this.setState({ uploadDelay: true });
this.uploadDelayTimeoutId = window.setTimeout(
() => {
this.setState({ uploadDelay: false });
},
UPLOAD_CHANGE_DELAY
);
}
}
// Last name
const lastNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameLabel',
});
const lastNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.lastNamePlaceholder',
});
const lastNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameRequired',
});
const lastNameRequired = validators.required(lastNameRequiredMessage);
componentWillUnmount() {
window.clearTimeout(this.blurTimeoutId);
}
const uploadingOverlay = uploadInProgress
? <div className={css.uploadingImageOverlay}><SpinnerIcon /></div>
: null;
render() {
const {
className,
currentUser,
form,
handleSubmit,
intl,
invalid,
onImageUpload,
pristine,
profileImage,
rootClassName,
submitting,
updateInProgress,
updateProfileError,
uploadImageError,
uploadInProgress,
} = this.props;
const hasUploadError = !!uploadImageError && !uploadInProgress;
const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError });
const transientUserProfileImage = profileImage.uploadedImage || user.profileImage;
const transientUser = { ...user, profileImage: transientUserProfileImage };
const avatarImage = uploadInProgress && profileImage.file
? <ImageFromFile
id={profileImage.id}
className={errorClasses}
rootClassName={css.uploadingImage}
aspectRatioClassName={css.squareAspectRatio}
file={profileImage.file}
>
{uploadingOverlay}
</ImageFromFile>
: <Avatar className={errorClasses} user={transientUser} />;
const user = ensureCurrentUser(currentUser);
const chooseAvatarLabel = profileImage.imageId || (uploadInProgress && profileImage.file)
? <div className={css.avatarContainer}>
{avatarImage}
<div className={css.changeAvatar}>
<FormattedMessage id="ProfileSettingsForm.changeAvatar" />
// First name
const firstNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameLabel',
});
const firstNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.firstNamePlaceholder',
});
const firstNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.firstNameRequired',
});
const firstNameRequired = validators.required(firstNameRequiredMessage);
// Last name
const lastNameLabel = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameLabel',
});
const lastNamePlaceholder = intl.formatMessage({
id: 'ProfileSettingsForm.lastNamePlaceholder',
});
const lastNameRequiredMessage = intl.formatMessage({
id: 'ProfileSettingsForm.lastNameRequired',
});
const lastNameRequired = validators.required(lastNameRequiredMessage);
const uploadingOverlay = uploadInProgress || this.state.uploadDelay
? <div className={css.uploadingImageOverlay}><SpinnerIcon /></div>
: null;
const hasUploadError = !!uploadImageError && !uploadInProgress;
const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError });
const transientUserProfileImage = profileImage.uploadedImage || user.profileImage;
const transientUser = { ...user, profileImage: transientUserProfileImage };
const fileUploadInProgress = uploadInProgress && profileImage.file;
const delayAfterUpload = profileImage.imageId && this.state.uploadDelay;
const imageFromFile = fileUploadInProgress || delayAfterUpload
? <ImageFromFile
id={profileImage.id}
className={errorClasses}
rootClassName={css.uploadingImage}
aspectRatioClassName={css.squareAspectRatio}
file={profileImage.file}
>
{uploadingOverlay}
</ImageFromFile>
: null;
// Avatar is rendered in hidden during the upload delay
// Upload delay smoothes image change process:
// responsive img has time to load srcset stuff before it is shown to user.
const avatarClasses = classNames(errorClasses, {
[css.avatarInvisible]: this.state.uploadDelay,
});
const avatarComponent = !fileUploadInProgress && profileImage.imageId
? <Avatar className={avatarClasses} user={transientUser} />
: null;
const chooseAvatarLabel = profileImage.imageId || fileUploadInProgress
? <div className={css.avatarContainer}>
{imageFromFile}
{avatarComponent}
<div className={css.changeAvatar}>
<FormattedMessage id="ProfileSettingsForm.changeAvatar" />
</div>
</div>
</div>
: <div className={css.avatarPlaceholder}>
<div className={css.avatarPlaceholderText}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePicture" />
: <div className={css.avatarPlaceholder}>
<div className={css.avatarPlaceholderText}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePicture" />
</div>
<div className={css.avatarPlaceholderTextMobile}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePictureMobile" />
</div>
</div>;
const submitError = updateProfileError
? <div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.updateProfileFailed" />
</div>
<div className={css.avatarPlaceholderTextMobile}>
<FormattedMessage id="ProfileSettingsForm.addYourProfilePictureMobile" />
</div>
</div>;
: null;
const submitError = updateProfileError
? <div className={css.error}>
<FormattedMessage id="ProfileSettingsForm.updateProfileFailed" />
</div>
: null;
const classes = classNames(rootClassName || css.root, className);
const inProgress = uploadInProgress || updateInProgress;
const submitDisabled = invalid || submitting || inProgress || pristine;
const classes = classNames(rootClassName || css.root, className);
const inProgress = uploadInProgress || updateInProgress;
const submitDisabled = invalid || submitting || inProgress || pristine;
return (
<form className={classes} onSubmit={handleSubmit}>
<div className={css.sectionContainer}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourProfilePicture" />
</h3>
<Field
accept={ACCEPT_IMAGES}
component={RenderAvatar}
disabled={uploadInProgress}
id="ProfileSettingsForm.changeAvatar"
label={chooseAvatarLabel}
name="profileImage"
onChange={onImageUpload}
type="file"
uploadImageError={uploadImageError}
/>
<div className={css.tip}><FormattedMessage id="ProfileSettingsForm.tip" /></div>
<div className={css.fileInfo}><FormattedMessage id="ProfileSettingsForm.fileInfo" /></div>
</div>
<div className={classNames(css.sectionContainer, css.lastSection)}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourName" />
</h3>
<div className={css.nameContainer}>
<TextInputField
className={css.firstName}
type="text"
name="firstName"
id={`${form}.firstName`}
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<TextInputField
className={css.lastName}
type="text"
name="lastName"
id={`${form}.lastName`}
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
return (
<form className={classes} onSubmit={handleSubmit}>
<div className={css.sectionContainer}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourProfilePicture" />
</h3>
<Field
accept={ACCEPT_IMAGES}
component={RenderAvatar}
disabled={uploadInProgress}
id="ProfileSettingsForm.changeAvatar"
label={chooseAvatarLabel}
name="profileImage"
onChange={onImageUpload}
type="file"
uploadImageError={uploadImageError}
/>
<div className={css.tip}><FormattedMessage id="ProfileSettingsForm.tip" /></div>
<div className={css.fileInfo}><FormattedMessage id="ProfileSettingsForm.fileInfo" /></div>
</div>
</div>
{submitError}
<Button className={css.submitButton} type="submit" disabled={submitDisabled}>
<FormattedMessage id="ProfileSettingsForm.saveChanges" />
</Button>
</form>
);
};
<div className={classNames(css.sectionContainer, css.lastSection)}>
<h3 className={css.sectionTitle}>
<FormattedMessage id="ProfileSettingsForm.yourName" />
</h3>
<div className={css.nameContainer}>
<TextInputField
className={css.firstName}
type="text"
name="firstName"
id={`${form}.firstName`}
label={firstNameLabel}
placeholder={firstNamePlaceholder}
validate={firstNameRequired}
/>
<TextInputField
className={css.lastName}
type="text"
name="lastName"
id={`${form}.lastName`}
label={lastNameLabel}
placeholder={lastNamePlaceholder}
validate={lastNameRequired}
/>
</div>
</div>
{submitError}
<Button className={css.submitButton} type="submit" disabled={submitDisabled}>
<FormattedMessage id="ProfileSettingsForm.saveChanges" />
</Button>
</form>
);
}
}
ProfileSettingsFormComponent.defaultProps = {
rootClassName: null,

View file

@ -3,7 +3,7 @@ import { currentUserShowSuccess } from '../../ducks/user.duck';
// ================ Action types ================ //
export const CLEAR_UPDATED_FORM = 'app/EditListingPage/CLEAR_UPDATED_FORM';
export const CLEAR_UPDATED_FORM = 'app/ProfileSettingsPage/CLEAR_UPDATED_FORM';
export const UPLOAD_IMAGE_REQUEST = 'app/ProfileSettingsPage/UPLOAD_IMAGE_REQUEST';
export const UPLOAD_IMAGE_SUCCESS = 'app/ProfileSettingsPage/UPLOAD_IMAGE_SUCCESS';
@ -55,11 +55,13 @@ export default function reducer(state = initialState, action = {}) {
case UPDATE_PROFILE_SUCCESS:
return {
...state,
image: null,
updateInProgress: false,
};
case UPDATE_PROFILE_ERROR:
return {
...state,
image: null,
updateInProgress: false,
updateProfileError: payload,
};

View file

@ -23,7 +23,9 @@ const onImageUploadHandler = (values, fn) => {
const onSubmit = (values, fn) => {
const { firstName, lastName, profileImage } = values;
const name = { firstName, lastName };
const updatedValues = profileImage.imageId
// Update profileImage only if file system has been accessed
const updatedValues = profileImage.imageId && profileImage.file
? { ...name, profileImageId: profileImage.imageId }
: name;