diff --git a/src/app/molecules/image-upload/ImageUpload.jsx b/src/app/molecules/image-upload/ImageUpload.jsx
new file mode 100644
index 0000000..da79489
--- /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 (
+
+
+ { (typeof imageSrc === 'string' || uploadPromise !== null) && (
+
+ )}
+
+
+ );
+}
+
+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 0000000..9e0f312
--- /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 0000000..a124aca
--- /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 (
+
+ );
+}
+
+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 0000000..10d62c7
--- /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 8914640..b20364c 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 (
+
+
+ )}
+ />
+
+ );
+}
+
function AppearanceSection() {
const [, updateState] = useState({});
@@ -104,6 +120,12 @@ function AboutSection() {
function Settings({ isOpen, onRequestClose }) {
const settingSections = [{
+ name: 'General',
+ iconSrc: SettingsIC,
+ render() {
+ return ;
+ },
+ }, {
name: 'Appearance',
iconSrc: SunIC,
render() {