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 ( +
+ + { (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 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 ( +
{ e.preventDefault(); saveDisplayName(); }} + > + handleAvatarUpload(null)} + /> +
+ + + +
+ + ); +} + +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 ( +
+ + )} + /> +
+ ); +} + 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() {