Added progress spinner in ImageUplaod (#91)
authorAjay Bura <ajbura@gmail.com>
Mon, 13 Sep 2021 06:57:55 +0000 (12:27 +0530)
committerAjay Bura <ajbura@gmail.com>
Mon, 13 Sep 2021 06:57:55 +0000 (12:27 +0530)
public/res/svg/avatar-clip.svg [deleted file]
src/app/molecules/image-upload/ImageUpload.jsx
src/app/molecules/image-upload/ImageUpload.scss
src/app/organisms/profile-editor/ProfileEditor.jsx
src/app/organisms/profile-editor/ProfileEditor.scss
src/app/organisms/settings/Settings.jsx

diff --git a/public/res/svg/avatar-clip.svg b/public/res/svg/avatar-clip.svg
deleted file mode 100644 (file)
index ffaa1a2..0000000
+++ /dev/null
@@ -1,3 +0,0 @@
-<svg width="80" height="80" viewBox="0 0 80 80" fill="none" xmlns="http://www.w3.org/2000/svg">
-<path fill-rule="evenodd" clip-rule="evenodd" d="M8 0C3.58173 0 0 3.58173 0 8V72C0 76.4183 3.58173 80 8 80H72C76.4183 80 80 76.4183 80 72V26H62C57.5817 26 54 22.4183 54 18V0H8Z" fill="#E24444"/>
-</svg>
index 992d5bca09e7dbacd79036b373f17198cbfc3abf..da794892532b7a88a0191bd6e7e73372f62888be 100644 (file)
@@ -1,48 +1,73 @@
-import React, { useRef } from 'react';
+import React, { useState, useRef } from 'react';
 import PropTypes from 'prop-types';
+import './ImageUpload.scss';
 
 import initMatrix from '../../../client/initMatrix';
 
-import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
+import Text from '../../atoms/text/Text';
 import Avatar from '../../atoms/avatar/Avatar';
-
-import RawIcon from '../../atoms/system-icons/RawIcon';
-import './ImageUpload.scss';
+import Spinner from '../../atoms/spinner/Spinner';
 
 function ImageUpload({
-  text, bgColor, imageSrc, onUpload,
+  text, bgColor, imageSrc, onUpload, onRequestRemove,
 }) {
+  const [uploadPromise, setUploadPromise] = useState(null);
   const uploadImageRef = useRef(null);
 
-  // Uploads image and passes resulting URI to onUpload function provided in component props.
-  function uploadImage(e) {
+  async function uploadImage(e) {
     const file = e.target.files.item(0);
-    if (file !== null) { // TODO Add upload progress spinner
-      initMatrix.matrixClient.uploadContent(file, { onlyContentUri: false }).then((res) => {
-        if (res.content_uri !== null) {
-          onUpload({ content_uri: res.content_uri });
-        }
-      }, (err) => {
-        console.log(err); // TODO Replace with alert banner.
-      });
+    if (file === null) return;
+    try {
+      const uPromise = initMatrix.matrixClient.uploadContent(file, { onlyContentUri: false });
+      setUploadPromise(uPromise);
+
+      const res = await uPromise;
+      if (typeof res?.content_uri === 'string') onUpload(res.content_uri);
+      setUploadPromise(null);
+    } catch {
+      setUploadPromise(null);
     }
+    uploadImageRef.current.value = null;
+  }
+
+  function cancelUpload() {
+    initMatrix.matrixClient.cancelUpload(uploadPromise);
+    setUploadPromise(null);
+    uploadImageRef.current.value = null;
   }
 
   return (
-    <button type="button" className="img-upload" onClick={() => { uploadImageRef.current.click(); }}>
-      <div className="img-upload__mask">
+    <div className="img-upload__wrapper">
+      <button
+        type="button"
+        className="img-upload"
+        onClick={() => {
+          if (uploadPromise !== null) return;
+          uploadImageRef.current.click();
+        }}
+      >
         <Avatar
           imageSrc={imageSrc}
           text={text.slice(0, 1)}
           bgColor={bgColor}
           size="large"
         />
-      </div>
-      <div className="img-upload__icon">
-        <RawIcon size="small" src={SettingsIC} />
-      </div>
+        <div className={`img-upload__process ${uploadPromise === null ? ' img-upload__process--stopped' : ''}`}>
+          {uploadPromise === null && <Text variant="b3">Upload</Text>}
+          {uploadPromise !== null && <Spinner size="small" />}
+        </div>
+      </button>
+      { (typeof imageSrc === 'string' || uploadPromise !== null) && (
+        <button
+          className="img-upload__btn-cancel"
+          type="button"
+          onClick={uploadPromise === null ? onRequestRemove : cancelUpload}
+        >
+          <Text variant="b3">{uploadPromise ? 'Cancel' : 'Remove'}</Text>
+        </button>
+      )}
       <input onChange={uploadImage} style={{ display: 'none' }} ref={uploadImageRef} type="file" />
-    </button>
+    </div>
   );
 }
 
@@ -50,14 +75,14 @@ ImageUpload.defaultProps = {
   text: null,
   bgColor: 'transparent',
   imageSrc: null,
-  onUpload: null,
 };
 
 ImageUpload.propTypes = {
   text: PropTypes.string,
   bgColor: PropTypes.string,
   imageSrc: PropTypes.string,
-  onUpload: PropTypes.func,
+  onUpload: PropTypes.func.isRequired,
+  onRequestRemove: PropTypes.func.isRequired,
 };
 
 export default ImageUpload;
index dbf2bace98f2a5e8174623c34ee1c9bda5460a3d..9e0f312f52d4edf7712d6e41bc8ed5ed1f05a9a2 100644 (file)
@@ -1,20 +1,50 @@
-.img-upload {
+.img-upload__wrapper {
        display: flex;
-       flex-direction: row-reverse;
-       width: 80px;
-       height: 80px;
+       flex-direction: column;
+       align-items: center;
 }
 
-.img-upload:hover {
+.img-upload {
+       display: flex;
        cursor: pointer;
-}
+       position: relative;
+
+       &__process {
+               width: 100%;
+               height: 100%;
+               border-radius: var(--bo-radius);
+               display: flex;
+               justify-content: center;
+               align-items: center;
+               background-color: rgba(0, 0, 0, .6);
+               
+               position: absolute;
+               left: 0;
+               right: 0;
+               z-index: 1;
+               & .text {
+                       text-transform: uppercase;
+                       font-weight: 600;
+                       color: white;
+               }
+               &--stopped {
+                       display: none;
+               }
+               & .donut-spinner {
+                       border-color: rgb(255, 255, 255, .3);
+       border-left-color: white;
+               }
+       }
+       &:hover .img-upload__process--stopped {
+               display: flex;
+       }
 
-.img-upload__mask {
-       mask: url('../../../../public/res/svg/avatar-clip.svg');
-       -webkit-mask: url('../../../../public/res/svg/avatar-clip.svg');
-}
 
-.img-upload__icon {
-  z-index: 1;
-  position: absolute;
-}
\ No newline at end of file
+       &__btn-cancel {
+               margin-top: var(--sp-extra-tight);
+               cursor: pointer;
+               & .text {
+                       color: var(--tc-danger-normal)
+               }
+       }
+}
index 61cd896d2b95efa1d99e97ab5a85ba542b0340b0..9dd308a4bf0b4793f45b191bb0b75a4796580898 100644 (file)
@@ -18,15 +18,22 @@ function ProfileEditor({
   const mx = initMatrix.matrixClient;
   const displayNameRef = useRef(null);
   const bgColor = colorMXID(userId);
-  const [imageSrc, updateImgSrc] = useState(mx.mxcUrlToHttp(mx.getUser(mx.getUserId()).avatarUrl));
+  const [avatarSrc, setAvatarSrc] = useState(mx.mxcUrlToHttp(mx.getUser(mx.getUserId()).avatarUrl, 80, 80, 'crop') || null);
   const [disabled, setDisabled] = useState(true);
 
   let username = mx.getUser(mx.getUserId()).displayName;
 
   // Sets avatar URL and updates the avatar component in profile editor to reflect new upload
-  function handleUpload(e) {
-    mx.setAvatarUrl(e.content_uri);
-    updateImgSrc(mx.mxcUrlToHttp(e.content_uri));
+  function handleAvatarUpload(url) {
+    if (url === null) {
+      if (confirm('Are you sure you want to remove avatar?')) {
+        mx.setAvatarUrl('');
+        setAvatarSrc(null);
+      }
+      return;
+    }
+    mx.setAvatarUrl(url);
+    setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
   }
 
   function saveDisplayName() {
@@ -44,7 +51,13 @@ function ProfileEditor({
 
   return (
     <form className="profile-editor">
-      <ImageUpload text={username} bgColor={bgColor} imageSrc={imageSrc} onUpload={handleUpload} />
+      <ImageUpload
+        text={username}
+        bgColor={bgColor}
+        imageSrc={avatarSrc}
+        onUpload={handleAvatarUpload}
+        onRequestRemove={() => handleAvatarUpload(null)}
+      />
       <div className="profile-editor__input-container">
         <Text variant="b3">
           Display name of&nbsp;
index 015b920f95f7b2f58afd81a9be57bb1fef8a1dab..882f0792649e540c2c2fa65711554130688cee88 100644 (file)
@@ -3,14 +3,10 @@
   align-items: end;
 }
 
-.img-upload {
-  margin-right: var(--sp-normal)
-}
-
 .profile-editor__input-container {
   display: flex;
   flex-direction: column;
-  margin-right: var(--sp-normal);
+  margin: 0 var(--sp-normal);
   width: 100%;
   max-width: 400px;
 }
index f97c942c1502a953887fa8e43a433452330b4d6d..b20364c604f8a530471c1177eb3f770d34da6341 100644 (file)
@@ -30,7 +30,7 @@ function GeneralSection() {
   return (
     <div className="settings-content">
       <SettingTile
-        title="Profile"
+        title=""
         content={(
           <ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
         )}