From c942f3b59846ae9b649eaf172f3d073801b9015b Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 11 Sep 2017 15:56:40 +0300 Subject: [PATCH 1/4] Send updated image only when file system is accessed --- src/containers/ProfileSettingsPage/ProfileSettingsPage.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js index a1c46a0e..6663629a 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.js @@ -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; From 040a1113465418a468a3ede5e774a10e7704be21 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 11 Sep 2017 19:12:37 +0300 Subject: [PATCH 2/4] Clear uploaded image data from store after submit --- .../ProfileSettingsPage/ProfileSettingsPage.duck.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js index 83abf1b5..560b3886 100644 --- a/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js +++ b/src/containers/ProfileSettingsPage/ProfileSettingsPage.duck.js @@ -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, }; From 7c2bbc5e2ce702f1340623830f53f25c89e9de3c Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 11 Sep 2017 18:58:17 +0300 Subject: [PATCH 3/4] ProfileSettingsForm extends Component --- .../ProfileSettingsForm.js | 264 +++++++++--------- 1 file changed, 133 insertions(+), 131 deletions(-) diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js index 7a8dce18..f0bf0c89 100644 --- a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js @@ -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'; @@ -58,148 +58,150 @@ 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 { + render() { + const { + className, + currentUser, + form, + handleSubmit, + intl, + invalid, + onImageUpload, + pristine, + profileImage, + rootClassName, + submitting, + updateInProgress, + updateProfileError, + uploadImageError, + uploadInProgress, + } = this.props; - const user = ensureCurrentUser(currentUser); + const user = ensureCurrentUser(currentUser); - // 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); + // 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); + // 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 - ?
- : null; + const uploadingOverlay = uploadInProgress + ?
+ : null; - 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 - ? - {uploadingOverlay} - - : ; + 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 + ? + {uploadingOverlay} + + : ; - const chooseAvatarLabel = profileImage.imageId || (uploadInProgress && profileImage.file) - ?
- {avatarImage} -
- + const chooseAvatarLabel = profileImage.imageId || (uploadInProgress && profileImage.file) + ?
+ {avatarImage} +
+ +
-
- :
-
- + :
+
+ +
+
+ +
+
; + + const submitError = updateProfileError + ?
+
-
- -
-
; + : null; - const submitError = updateProfileError - ?
- -
- : 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 ( -
-
-

- -

- -
-
-
-
-

- -

-
- - +
+

+ +

+ +
+
-
- {submitError} - - - ); +
+

+ +

+
+ + +
+
+ {submitError} + + + ); + } }; ProfileSettingsFormComponent.defaultProps = { From 7b7d0c16caac778f40d457bfce1d3297a05e8143 Mon Sep 17 00:00:00 2001 From: Vesa Luusua Date: Mon, 11 Sep 2017 19:05:48 +0300 Subject: [PATCH 4/4] Give Avatar time to load its responsive srcset stuff --- .../ProfileSettingsForm.css | 7 +++ .../ProfileSettingsForm.js | 52 ++++++++++++++++--- 2 files changed, 53 insertions(+), 6 deletions(-) diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.css b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css index 5097c3fc..c7237f23 100644 --- a/src/containers/ProfileSettingsForm/ProfileSettingsForm.css +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.css @@ -223,6 +223,13 @@ padding-bottom: 100%; } +.avatarInvisible { + visibility: hidden; + position: absolute; + top: -1000px; + left: -1000px; +} + .tip { @apply --marketplaceDefaultFontStyles; color: var(--matterColorAnti); diff --git a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js index f0bf0c89..31c96688 100644 --- a/src/containers/ProfileSettingsForm/ProfileSettingsForm.js +++ b/src/containers/ProfileSettingsForm/ProfileSettingsForm.js @@ -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; @@ -59,6 +60,31 @@ RenderAvatar.propTypes = { }; class ProfileSettingsFormComponent extends Component { + constructor(props) { + super(props); + + this.uploadDelayTimeoutId = null; + this.state = { uploadDelay: false }; + } + + 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 + ); + } + } + + componentWillUnmount() { + window.clearTimeout(this.blurTimeoutId); + } + render() { const { className, @@ -104,7 +130,7 @@ class ProfileSettingsFormComponent extends Component { }); const lastNameRequired = validators.required(lastNameRequiredMessage); - const uploadingOverlay = uploadInProgress + const uploadingOverlay = uploadInProgress || this.state.uploadDelay ?
: null; @@ -112,7 +138,10 @@ class ProfileSettingsFormComponent extends Component { const errorClasses = classNames({ [css.avatarUploadError]: hasUploadError }); const transientUserProfileImage = profileImage.uploadedImage || user.profileImage; const transientUser = { ...user, profileImage: transientUserProfileImage }; - const avatarImage = uploadInProgress && profileImage.file + + const fileUploadInProgress = uploadInProgress && profileImage.file; + const delayAfterUpload = profileImage.imageId && this.state.uploadDelay; + const imageFromFile = fileUploadInProgress || delayAfterUpload ? {uploadingOverlay} - : ; + : null; - const chooseAvatarLabel = profileImage.imageId || (uploadInProgress && profileImage.file) + // 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 + ? + : null; + + const chooseAvatarLabel = profileImage.imageId || fileUploadInProgress ?
- {avatarImage} + {imageFromFile} + {avatarComponent}
@@ -202,7 +242,7 @@ class ProfileSettingsFormComponent extends Component { ); } -}; +} ProfileSettingsFormComponent.defaultProps = { rootClassName: null,