diff --git a/src/app/molecules/image-upload/ImageUpload.jsx b/src/app/molecules/image-upload/ImageUpload.jsx
new file mode 100644
index 00000000..da794892
--- /dev/null
+++ b/src/app/molecules/image-upload/ImageUpload.jsx
@@ -0,0 +1,88 @@
+import React, { useState, useRef } from 'react';
+import PropTypes from 'prop-types';
+import './ImageUpload.scss';
+
+import initMatrix from '../../../client/initMatrix';
+
+import Text from '../../atoms/text/Text';
+import Avatar from '../../atoms/avatar/Avatar';
+import Spinner from '../../atoms/spinner/Spinner';
+
+function ImageUpload({
+  text, bgColor, imageSrc, onUpload, onRequestRemove,
+}) {
+  const [uploadPromise, setUploadPromise] = useState(null);
+  const uploadImageRef = useRef(null);
+
+  async function uploadImage(e) {
+    const file = e.target.files.item(0);
+    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 (
+    <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 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" />
+    </div>
+  );
+}
+
+ImageUpload.defaultProps = {
+  text: null,
+  bgColor: 'transparent',
+  imageSrc: null,
+};
+
+ImageUpload.propTypes = {
+  text: PropTypes.string,
+  bgColor: PropTypes.string,
+  imageSrc: PropTypes.string,
+  onUpload: PropTypes.func.isRequired,
+  onRequestRemove: PropTypes.func.isRequired,
+};
+
+export default ImageUpload;
diff --git a/src/app/molecules/image-upload/ImageUpload.scss b/src/app/molecules/image-upload/ImageUpload.scss
new file mode 100644
index 00000000..9e0f312f
--- /dev/null
+++ b/src/app/molecules/image-upload/ImageUpload.scss
@@ -0,0 +1,50 @@
+.img-upload__wrapper {
+	display: flex;
+	flex-direction: column;
+	align-items: center;
+}
+
+.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;
+	}
+
+
+	&__btn-cancel {
+		margin-top: var(--sp-extra-tight);
+		cursor: pointer;
+		& .text {
+			color: var(--tc-danger-normal)
+		}
+	}
+}
diff --git a/src/app/organisms/profile-editor/ProfileEditor.jsx b/src/app/organisms/profile-editor/ProfileEditor.jsx
new file mode 100644
index 00000000..a124acaa
--- /dev/null
+++ b/src/app/organisms/profile-editor/ProfileEditor.jsx
@@ -0,0 +1,90 @@
+import React, { useState, useRef } from 'react';
+import PropTypes from 'prop-types';
+
+import initMatrix from '../../../client/initMatrix';
+import colorMXID from '../../../util/colorMXID';
+
+import Button from '../../atoms/button/Button';
+import ImageUpload from '../../molecules/image-upload/ImageUpload';
+import Input from '../../atoms/input/Input';
+import Text from '../../atoms/text/Text';
+
+import './ProfileEditor.scss';
+
+// TODO Fix bug that prevents 'Save' button from enabling up until second changed.
+function ProfileEditor({
+  userId,
+}) {
+  const mx = initMatrix.matrixClient;
+  const displayNameRef = useRef(null);
+  const bgColor = colorMXID(userId);
+  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 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() {
+    const newDisplayName = displayNameRef.current.value;
+    if (newDisplayName !== null && newDisplayName !== username) {
+      mx.setDisplayName(newDisplayName);
+      username = newDisplayName;
+      setDisabled(true);
+    }
+  }
+
+  function onDisplayNameInputChange() {
+    setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
+  }
+  function cancelDisplayNameChanges() {
+    displayNameRef.current.value = username;
+    onDisplayNameInputChange();
+  }
+
+  return (
+    <form
+      className="profile-editor"
+      onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
+    >
+      <ImageUpload
+        text={username}
+        bgColor={bgColor}
+        imageSrc={avatarSrc}
+        onUpload={handleAvatarUpload}
+        onRequestRemove={() => handleAvatarUpload(null)}
+      />
+      <div className="profile-editor__input-wrapper">
+        <Input
+          label={`Display name of ${mx.getUserId()}`}
+          onChange={onDisplayNameInputChange}
+          value={mx.getUser(mx.getUserId()).displayName}
+          forwardRef={displayNameRef}
+        />
+        <Button variant="primary" type="submit" disabled={disabled}>Save</Button>
+        <Button onClick={cancelDisplayNameChanges}>Cancel</Button>
+      </div>
+    </form>
+  );
+}
+
+ProfileEditor.defaultProps = {
+  userId: null,
+};
+
+ProfileEditor.propTypes = {
+  userId: PropTypes.string,
+};
+
+export default ProfileEditor;
diff --git a/src/app/organisms/profile-editor/ProfileEditor.scss b/src/app/organisms/profile-editor/ProfileEditor.scss
new file mode 100644
index 00000000..10d62c75
--- /dev/null
+++ b/src/app/organisms/profile-editor/ProfileEditor.scss
@@ -0,0 +1,30 @@
+.profile-editor {
+  display: flex;
+  align-items: flex-start;
+}
+
+.profile-editor__input-wrapper {
+  flex: 1;
+  min-width: 0;
+  margin-top: 10px;
+  
+  display: flex;
+  align-items: flex-end;
+  flex-wrap: wrap;
+
+  & > .input-container  {
+    flex: 1;
+  }
+  & > button {
+    height: 46px;
+    margin-top: var(--sp-normal);
+  }
+
+  & > * {
+    margin-left: var(--sp-normal);
+    [dir=rtl] & {
+      margin-left: 0;
+      margin-right: var(--sp-normal);
+    }
+  }
+}
\ No newline at end of file
diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx
index 8914640d..b20364c6 100644
--- a/src/app/organisms/settings/Settings.jsx
+++ b/src/app/organisms/settings/Settings.jsx
@@ -16,6 +16,9 @@ import PopupWindow, { PWContentSelector } from '../../molecules/popup-window/Pop
 import SettingTile from '../../molecules/setting-tile/SettingTile';
 import ImportE2ERoomKeys from '../../molecules/import-e2e-room-keys/ImportE2ERoomKeys';
 
+import ProfileEditor from '../profile-editor/ProfileEditor';
+
+import SettingsIC from '../../../../public/res/ic/outlined/settings.svg';
 import SunIC from '../../../../public/res/ic/outlined/sun.svg';
 import LockIC from '../../../../public/res/ic/outlined/lock.svg';
 import InfoIC from '../../../../public/res/ic/outlined/info.svg';
@@ -23,6 +26,19 @@ import CrossIC from '../../../../public/res/ic/outlined/cross.svg';
 
 import CinnySVG from '../../../../public/res/svg/cinny.svg';
 
+function GeneralSection() {
+  return (
+    <div className="settings-content">
+      <SettingTile
+        title=""
+        content={(
+          <ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
+        )}
+      />
+    </div>
+  );
+}
+
 function AppearanceSection() {
   const [, updateState] = useState({});
 
@@ -104,6 +120,12 @@ function AboutSection() {
 
 function Settings({ isOpen, onRequestClose }) {
   const settingSections = [{
+    name: 'General',
+    iconSrc: SettingsIC,
+    render() {
+      return <GeneralSection />;
+    },
+  }, {
     name: 'Appearance',
     iconSrc: SunIC,
     render() {