Redesign app/user settings (#404)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Mon, 21 Mar 2022 04:16:11 +0000 (09:46 +0530)
committerGitHub <noreply@github.com>
Mon, 21 Mar 2022 04:16:11 +0000 (09:46 +0530)
* Redesign app settings

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Redesign user profile in settings

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Update string

Signed-off-by: Ajay Bura <ajbura@gmail.com>
* Fix bug

Signed-off-by: Ajay Bura <ajbura@gmail.com>
src/app/atoms/tabs/Tabs.jsx
src/app/organisms/navigation/SideBar.jsx
src/app/organisms/profile-editor/ProfileEditor.jsx
src/app/organisms/profile-editor/ProfileEditor.scss
src/app/organisms/pw/Windows.jsx
src/app/organisms/settings/Settings.jsx
src/app/organisms/settings/Settings.scss
src/app/organisms/space-settings/SpaceSettings.scss
src/client/action/navigation.js
src/client/state/navigation.js

index 5426cf31482e40aa3c9d61da3e0e55f718d960bb..39800ce350dbcbcb3f11c5423766792e588de2fd 100644 (file)
@@ -74,7 +74,7 @@ Tabs.defaultProps = {
 
 Tabs.propTypes = {
   items: PropTypes.arrayOf(
-    PropTypes.exact({
+    PropTypes.shape({
       iconSrc: PropTypes.string,
       text: PropTypes.string,
       disabled: PropTypes.bool,
@@ -84,4 +84,4 @@ Tabs.propTypes = {
   onSelect: PropTypes.func.isRequired,
 };
 
-export { Tabs as default };
+export default Tabs;
index 4a305227488ff3d5545ab196957c1b418813a3b5..54723bddadd90ca9813ad7cd37b4fc25919b5032 100644 (file)
@@ -72,7 +72,7 @@ function ProfileAvatarMenu() {
   return (
     <SidebarAvatar
       onClick={openSettings}
-      tooltip={profile.displayName}
+      tooltip="Settings"
       avatar={(
         <Avatar
           text={profile.displayName}
index 82e6579485a7302ade13c58765abb8247fd5b3a5..038c2175b05983771a81653b71c309bf0cd5f837 100644 (file)
@@ -1,35 +1,44 @@
 import React, { useState, useEffect, useRef } from 'react';
 import PropTypes from 'prop-types';
+import { twemojify } from '../../../util/twemojify';
 
 import initMatrix from '../../../client/initMatrix';
 import colorMXID from '../../../util/colorMXID';
 
+import Text from '../../atoms/text/Text';
+import IconButton from '../../atoms/button/IconButton';
 import Button from '../../atoms/button/Button';
 import ImageUpload from '../../molecules/image-upload/ImageUpload';
 import Input from '../../atoms/input/Input';
 
+import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
+
 import './ProfileEditor.scss';
 
 // TODO Fix bug that prevents 'Save' button from enabling up until second changed.
-function ProfileEditor({
-  userId,
-}) {
+function ProfileEditor({ userId }) {
+  const [isEditing, setIsEditing] = useState(false);
   const mx = initMatrix.matrixClient;
+  const user = mx.getUser(mx.getUserId());
+
   const displayNameRef = useRef(null);
-  const bgColor = colorMXID(userId);
-  const [avatarSrc, setAvatarSrc] = useState(null);
+  const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
+  const [username, setUsername] = useState(user.displayName);
   const [disabled, setDisabled] = useState(true);
 
-  let username = mx.getUser(mx.getUserId()).displayName;
-
   useEffect(() => {
+    let isMounted = true;
     mx.getProfileInfo(mx.getUserId()).then((info) => {
+      if (!isMounted) return;
       setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
+      setUsername(info.displayname);
     });
+    return () => {
+      isMounted = false;
+    };
   }, [userId]);
 
-  // Sets avatar URL and updates the avatar component in profile editor to reflect new upload
-  function handleAvatarUpload(url) {
+  const handleAvatarUpload = (url) => {
     if (url === null) {
       if (confirm('Are you sure you want to remove avatar?')) {
         mx.setAvatarUrl('');
@@ -39,48 +48,72 @@ function ProfileEditor({
     }
     mx.setAvatarUrl(url);
     setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
-  }
+  };
 
-  function saveDisplayName() {
+  const saveDisplayName = () => {
     const newDisplayName = displayNameRef.current.value;
     if (newDisplayName !== null && newDisplayName !== username) {
       mx.setDisplayName(newDisplayName);
-      username = newDisplayName;
+      setUsername(newDisplayName);
       setDisabled(true);
+      setIsEditing(false);
     }
-  }
+  };
 
-  function onDisplayNameInputChange() {
+  const onDisplayNameInputChange = () => {
     setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
-  }
-  function cancelDisplayNameChanges() {
+  };
+  const cancelDisplayNameChanges = () => {
     displayNameRef.current.value = username;
     onDisplayNameInputChange();
-  }
+    setIsEditing(false);
+  };
 
-  return (
+  const renderForm = () => (
     <form
-      className="profile-editor"
+      className="profile-editor__form"
+      style={{ marginBottom: avatarSrc ? '24px' : '0' }}
       onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }}
     >
+      <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>
+    </form>
+  );
+
+  const renderInfo = () => (
+    <div className="profile-editor__info" style={{ marginBottom: avatarSrc ? '24px' : '0' }}>
+      <div>
+        <Text variant="h2" primary weight="medium">{twemojify(username)}</Text>
+        <IconButton
+          src={PencilIC}
+          size="extra-small"
+          tooltip="Edit"
+          onClick={() => setIsEditing(true)}
+        />
+      </div>
+      <Text variant="b2">{mx.getUserId()}</Text>
+    </div>
+  );
+
+  return (
+    <div className="profile-editor">
       <ImageUpload
         text={username}
-        bgColor={bgColor}
+        bgColor={colorMXID(userId)}
         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>
+      {
+        isEditing ? renderForm() : renderInfo()
+      }
+    </div>
   );
 }
 
index 9f95e5b3fb96463afffccf4d96e2eefb96eebf40..2e2ef9177b352785485149d3a3dc81668841124f 100644 (file)
@@ -1,28 +1,41 @@
 @use '../../partials/dir';
+@use '../../partials/flex';
 
 .profile-editor {
   display: flex;
-  align-items: flex-start;
+  align-items: flex-end;
 }
 
-.profile-editor__input-wrapper {
-  flex: 1;
-  min-width: 0;
-  margin-top: 10px;
-  
+.profile-editor__info,
+.profile-editor__form {
+  @extend .cp-fx__item-one;
+  @include dir.side(margin, var(--sp-loose), 0);
   display: flex;
-  align-items: flex-end;
+}
+
+.profile-editor__info {
+  flex-direction: column;
+  & > div:first-child {
+    display: flex;
+    align-items: center;
+  }
+  .ic-btn {
+    margin: 0 var(--sp-extra-tight);
+  }
+}
+
+.profile-editor__form {
+  margin-top: 10px;
   flex-wrap: wrap;
+  align-items: flex-end;
 
   & > .input-container  {
-    flex: 1;
+    @extend .cp-fx__item-one;
   }
   & > button {
     height: 46px;
     margin-top: var(--sp-normal);
-  }
-
-  & > * {
     @include dir.side(margin, var(--sp-normal), 0);
   }
+
 }
\ No newline at end of file
index ae2bc1a68a227b7de5b43ec7798f811f23f4ceca..ba80f132dbba575ca0a24ae6906a708f6c45729a 100644 (file)
@@ -18,7 +18,6 @@ function Windows() {
   const [inviteUser, changeInviteUser] = useState({
     isOpen: false, roomId: undefined, term: undefined,
   });
-  const [settings, changeSettings] = useState(false);
 
   function openInviteList() {
     changeInviteList(true);
@@ -36,20 +35,15 @@ function Windows() {
       searchTerm,
     });
   }
-  function openSettings() {
-    changeSettings(true);
-  }
 
   useEffect(() => {
     navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
     navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
     navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
-    navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
     return () => {
       navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
       navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
       navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
-      navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
     };
   }, []);
 
@@ -70,10 +64,7 @@ function Windows() {
         searchTerm={inviteUser.searchTerm}
         onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
       />
-      <Settings
-        isOpen={settings}
-        onRequestClose={() => changeSettings(false)}
-      />
+      <Settings />
       <SpaceSettings />
       <SpaceManage />
     </>
index 87f2766033d01d832eaeb65644ce92f8828edb35..84013cc94511e3fad7d15df69decd621d609d446 100644 (file)
@@ -1,10 +1,10 @@
-import React, { useState } from 'react';
-import PropTypes from 'prop-types';
+import React, { useState, useEffect } from 'react';
 import './Settings.scss';
 
 import initMatrix from '../../../client/initMatrix';
 import cons from '../../../client/state/cons';
 import settings from '../../../client/state/settings';
+import navigation from '../../../client/state/navigation';
 import {
   toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
   toggleNotifications, toggleNotificationSounds,
@@ -16,16 +16,17 @@ import Text from '../../atoms/text/Text';
 import IconButton from '../../atoms/button/IconButton';
 import Button from '../../atoms/button/Button';
 import Toggle from '../../atoms/button/Toggle';
+import Tabs from '../../atoms/tabs/Tabs';
+import { MenuHeader } from '../../atoms/context-menu/ContextMenu';
 import SegmentedControls from '../../atoms/segmented-controls/SegmentedControls';
 
-import PopupWindow, { PWContentSelector } from '../../molecules/popup-window/PopupWindow';
+import PopupWindow from '../../molecules/popup-window/PopupWindow';
 import SettingTile from '../../molecules/setting-tile/SettingTile';
 import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
 import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
 
 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 BellIC from '../../../../public/res/ic/outlined/bell.svg';
@@ -35,85 +36,74 @@ 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({});
 
   return (
-    <div className="settings-content">
-      <SettingTile
-        title="Follow system theme"
-        options={(
-          <Toggle
-            isActive={settings.useSystemTheme}
-            onToggle={() => { toggleSystemTheme(); updateState({}); }}
-          />
-        )}
-        content={<Text variant="b3">Use light or dark mode based on the system's settings.</Text>}
-      />
-      {(() => {
-        if (!settings.useSystemTheme) {
-          return (
-            <SettingTile
-              title="Theme"
-              content={(
-                <SegmentedControls
-                  selected={settings.getThemeIndex()}
-                  segments={[
-                    { text: 'Light' },
-                    { text: 'Silver' },
-                    { text: 'Dark' },
-                    { text: 'Butter' },
-                  ]}
-                  onSelect={(index) => settings.setTheme(index)}
-                />
-            )}
+    <div className="settings-appearance">
+      <div className="settings-appearance__card">
+        <MenuHeader>Theme</MenuHeader>
+        <SettingTile
+          title="Follow system theme"
+          options={(
+            <Toggle
+              isActive={settings.useSystemTheme}
+              onToggle={() => { toggleSystemTheme(); updateState({}); }}
             />
-          );
-        }
-      })()}
-      <SettingTile
-        title="Markdown formatting"
-        options={(
-          <Toggle
-            isActive={settings.isMarkdown}
-            onToggle={() => { toggleMarkdown(); updateState({}); }}
-          />
-        )}
-        content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
-      />
-      <SettingTile
-        title="Hide membership events"
-        options={(
-          <Toggle
-            isActive={settings.hideMembershipEvents}
-            onToggle={() => { toggleMembershipEvents(); updateState({}); }}
-          />
-        )}
-        content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
-      />
-      <SettingTile
-        title="Hide nick/avatar events"
-        options={(
-          <Toggle
-            isActive={settings.hideNickAvatarEvents}
-            onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
+          )}
+          content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
+        />
+        {!settings.useSystemTheme && (
+          <SettingTile
+            title="Theme"
+            content={(
+              <SegmentedControls
+                selected={settings.getThemeIndex()}
+                segments={[
+                  { text: 'Light' },
+                  { text: 'Silver' },
+                  { text: 'Dark' },
+                  { text: 'Butter' },
+                ]}
+                onSelect={(index) => settings.setTheme(index)}
+              />
+          )}
           />
         )}
-        content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
-      />
+      </div>
+      <div className="settings-appearance__card">
+        <MenuHeader>Room messages</MenuHeader>
+        <SettingTile
+          title="Markdown formatting"
+          options={(
+            <Toggle
+              isActive={settings.isMarkdown}
+              onToggle={() => { toggleMarkdown(); updateState({}); }}
+            />
+          )}
+          content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
+        />
+        <SettingTile
+          title="Hide membership events"
+          options={(
+            <Toggle
+              isActive={settings.hideMembershipEvents}
+              onToggle={() => { toggleMembershipEvents(); updateState({}); }}
+            />
+          )}
+          content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>}
+        />
+        <SettingTile
+          title="Hide nick/avatar events"
+          options={(
+            <Toggle
+              isActive={settings.hideNickAvatarEvents}
+              onToggle={() => { toggleNickAvatarEvents(); updateState({}); }}
+            />
+          )}
+          content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
+        />
+      </div>
     </div>
   );
 }
@@ -125,7 +115,7 @@ function NotificationsSection() {
 
   const renderOptions = () => {
     if (window.Notification === undefined) {
-      return <Text className="set-notifications__not-supported">Not supported in this browser.</Text>;
+      return <Text className="settings-notifications__not-supported">Not supported in this browser.</Text>;
     }
 
     if (permission === 'granted') {
@@ -152,21 +142,22 @@ function NotificationsSection() {
   };
 
   return (
-    <div className="set-notifications settings-content">
+    <div className="settings-notifications">
+      <MenuHeader>Notification & Sound</MenuHeader>
       <SettingTile
-        title="Show desktop notifications"
+        title="Desktop notification"
         options={renderOptions()}
-        content={<Text variant="b3">Show notifications when new messages arrive.</Text>}
+        content={<Text variant="b3">Show desktop notification when new messages arrive.</Text>}
       />
       <SettingTile
-        title="Play notification sounds"
+        title="Notification Sound"
         options={(
           <Toggle
             isActive={settings.isNotificationSounds}
             onToggle={() => { toggleNotificationSounds(); updateState({}); }}
           />
           )}
-        content={<Text variant="b3">Play sound when new messages arrive.</Text>}
+        content={<Text variant="b3">Play sound when new messages arrive.</Text>}
       />
     </div>
   );
@@ -174,153 +165,173 @@ function NotificationsSection() {
 
 function SecuritySection() {
   return (
-    <div className="set-security settings-content">
-      <SettingTile
-        title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
-      />
-      <SettingTile
-        title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
-        content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
-      />
-      <SettingTile
-        title="Export E2E room keys"
-        content={(
-          <>
-            <Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
-            <ExportE2ERoomKeys />
-          </>
-        )}
-      />
-      <SettingTile
-        title="Import E2E room keys"
-        content={(
-          <>
-            <Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
-            <ImportE2ERoomKeys />
-          </>
-        )}
-      />
+    <div className="settings-security">
+      <div className="settings-security__card">
+        <MenuHeader>Device Info</MenuHeader>
+        <SettingTile
+          title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
+        />
+        <SettingTile
+          title={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
+          content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
+        />
+      </div>
+      <div className="settings-security__card">
+        <MenuHeader>Encryption</MenuHeader>
+        <SettingTile
+          title="Export E2E room keys"
+          content={(
+            <>
+              <Text variant="b3">Export end-to-end encryption room keys to decrypt old messages in other session. In order to encrypt keys you need to set a password, which will be used while importing.</Text>
+              <ExportE2ERoomKeys />
+            </>
+          )}
+        />
+        <SettingTile
+          title="Import E2E room keys"
+          content={(
+            <>
+              <Text variant="b3">{'To decrypt older messages, Export E2EE room keys from Element (Settings > Security & Privacy > Encryption > Cryptography) and import them here. Imported keys are encrypted so you\'ll have to enter the password you set in order to decrypt it.'}</Text>
+              <ImportE2ERoomKeys />
+            </>
+          )}
+        />
+      </div>
     </div>
   );
 }
 
 function AboutSection() {
   return (
-    <div className="settings-content set__about">
-      <div className="set-about__branding">
-        <img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
-        <div>
-          <Text variant="h2" weight="medium">
-            Cinny
-            <span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
-          </Text>
-          <Text>Yet another matrix client</Text>
+    <div className="settings-about">
+      <div className="settings-about__card">
+        <MenuHeader>Application</MenuHeader>
+        <div className="settings-about__branding">
+          <img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
+          <div>
+            <Text variant="h2" weight="medium">
+              Cinny
+              <span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
+            </Text>
+            <Text>Yet another matrix client</Text>
 
-          <div className="set-about__btns">
-            <Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
-            <Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
+            <div className="settings-about__btns">
+              <Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
+              <Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
+            </div>
           </div>
         </div>
       </div>
-      <div className="set-about__credits">
-        <Text variant="s1" weight="medium">Credits</Text>
-        <ul>
-          <li>
-            {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
-            <Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
-          </li>
-          <li>
-            {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
-            <Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
-          </li>
-          <li>
-            {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
-            <Text>The <a href="https://material.io/design/sound/sound-resources.html" target="_blank" rel="noreferrer noopener">Material sound resources</a> are © <a href="https://google.com" target="_blank" rel="noreferrer noopener">Google</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
-          </li>
-        </ul>
+      <div className="settings-about__card">
+        <MenuHeader>Credits</MenuHeader>
+        <div className="settings-about__credits">
+          <ul>
+            <li>
+              {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
+              <Text>The <a href="https://github.com/matrix-org/matrix-js-sdk" rel="noreferrer noopener" target="_blank">matrix-js-sdk</a> is © <a href="https://matrix.org/foundation" rel="noreferrer noopener" target="_blank">The Matrix.org Foundation C.I.C</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" rel="noreferrer noopener" target="_blank">Apache 2.0</a>.</Text>
+            </li>
+            <li>
+              {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
+              <Text>The <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twemoji</a> emoji art is © <a href="https://twemoji.twitter.com" target="_blank" rel="noreferrer noopener">Twitter, Inc and other contributors</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
+            </li>
+            <li>
+              {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
+              <Text>The <a href="https://material.io/design/sound/sound-resources.html" target="_blank" rel="noreferrer noopener">Material sound resources</a> are © <a href="https://google.com" target="_blank" rel="noreferrer noopener">Google</a> used under the terms of <a href="https://creativecommons.org/licenses/by/4.0/" target="_blank" rel="noreferrer noopener">CC-BY 4.0</a>.</Text>
+            </li>
+          </ul>
+        </div>
       </div>
     </div>
   );
 }
 
-function Settings({ isOpen, onRequestClose }) {
-  const settingSections = [{
-    name: 'General',
-    iconSrc: SettingsIC,
-    render() {
-      return <GeneralSection />;
-    },
-  }, {
-    name: 'Appearance',
-    iconSrc: SunIC,
-    render() {
-      return <AppearanceSection />;
-    },
-  }, {
-    name: 'Notifications',
-    iconSrc: BellIC,
-    render() {
-      return <NotificationsSection />;
-    },
-  }, {
-    name: 'Security & Privacy',
-    iconSrc: LockIC,
-    render() {
-      return <SecuritySection />;
-    },
-  }, {
-    name: 'Help & About',
-    iconSrc: InfoIC,
-    render() {
-      return <AboutSection />;
-    },
-  }];
-  const [selectedSection, setSelectedSection] = useState(settingSections[0]);
+const tabText = {
+  APPEARANCE: 'Appearance',
+  NOTIFICATIONS: 'Notifications',
+  SECURITY: 'Security',
+  ABOUT: 'About',
+};
+const tabItems = [{
+  text: tabText.APPEARANCE,
+  iconSrc: SunIC,
+  disabled: false,
+  render: () => <AppearanceSection />,
+}, {
+  text: tabText.NOTIFICATIONS,
+  iconSrc: BellIC,
+  disabled: false,
+  render: () => <NotificationsSection />,
+}, {
+  text: tabText.SECURITY,
+  iconSrc: LockIC,
+  disabled: false,
+  render: () => <SecuritySection />,
+}, {
+  text: tabText.ABOUT,
+  iconSrc: InfoIC,
+  disabled: false,
+  render: () => <AboutSection />,
+}];
+
+function useWindowToggle(setSelectedTab) {
+  const [isOpen, setIsOpen] = useState(false);
+
+  useEffect(() => {
+    const openSettings = (tab) => {
+      const tabItem = tabItems.find((item) => item.text === tab);
+      if (tabItem) setSelectedTab(tabItem);
+      setIsOpen(true);
+    };
+    navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
+    return () => {
+      navigation.removeListener(cons.events.navigation.SETTINGS_OPENED, openSettings);
+    };
+  }, []);
+
+  const requestClose = () => setIsOpen(false);
+
+  return [isOpen, requestClose];
+}
+
+function Settings() {
+  const [selectedTab, setSelectedTab] = useState(tabItems[0]);
+  const [isOpen, requestClose] = useWindowToggle(setSelectedTab);
 
+  const handleTabChange = (tabItem) => setSelectedTab(tabItem);
   const handleLogout = () => {
     if (confirm('Confirm logout')) logout();
   };
 
   return (
     <PopupWindow
-      className="settings-window"
       isOpen={isOpen}
-      onRequestClose={onRequestClose}
-      title="Settings"
-      contentTitle={selectedSection.name}
-      drawer={(
+      className="settings-window"
+      title={<Text variant="s1" weight="medium" primary>Settings</Text>}
+      contentOptions={(
         <>
-          {
-            settingSections.map((section) => (
-              <PWContentSelector
-                key={section.name}
-                selected={selectedSection.name === section.name}
-                onClick={() => setSelectedSection(section)}
-                iconSrc={section.iconSrc}
-              >
-                {section.name}
-              </PWContentSelector>
-            ))
-          }
-          <PWContentSelector
-            variant="danger"
-            onClick={handleLogout}
-            iconSrc={PowerIC}
-          >
+          <Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
             Logout
-          </PWContentSelector>
+          </Button>
+          <IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />
         </>
       )}
-      contentOptions={<IconButton src={CrossIC} onClick={onRequestClose} tooltip="Close" />}
+      onRequestClose={requestClose}
     >
-      {selectedSection.render()}
+      {isOpen && (
+        <div className="settings-window__content">
+          <ProfileEditor userId={initMatrix.matrixClient.getUserId()} />
+          <Tabs
+            items={tabItems}
+            defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
+            onSelect={handleTabChange}
+          />
+          <div className="settings-window__cards-wrapper">
+            { selectedTab.render() }
+          </div>
+        </div>
+      )}
     </PopupWindow>
   );
 }
 
-Settings.propTypes = {
-  isOpen: PropTypes.bool.isRequired,
-  onRequestClose: PropTypes.func.isRequired,
-};
-
 export default Settings;
index 54451222d459e32c93fdded57196a5b489e11b8d..2d086c6d61606f1dcfe2397c694ea7874af01e51 100644 (file)
@@ -2,58 +2,86 @@
 @use '../../partials/dir';
 
 .settings-window {
-  & .pw__drawer__content {
-    @extend .cp-fx__column;
-    min-height: 100%;
-    padding-bottom: var(--sp-extra-tight);
+  & .pw {
+    background-color: var(--bg-surface-low);
+  }
+  
+  .header .btn-danger {
+    margin: 0 var(--sp-tight);
+    box-shadow: none;
+  }
 
-    & > .pw-content-selector:last-child {
-      margin-top: auto;
-    }
+  & .profile-editor {
+    padding: var(--sp-loose) var(--sp-extra-loose);
+  } 
+
+  & .tabs__content {
+    padding: 0 var(--sp-normal);
   }
-  & .pw__content-container {
-    min-height: 100%;
+
+  &__cards-wrapper {
+    padding: 0 var(--sp-normal);
+    @include dir.side(padding, var(--sp-normal), var(--sp-extra-tight));
   }
 }
+.settings-window__card {
+  margin: var(--sp-normal) 0;
+  background-color: var(--bg-surface);
+  border-radius: var(--bo-radius);
+  box-shadow: var(--bs-surface-border);
+  overflow: hidden;
 
-.settings-content {
-  @include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
+  & > .context-menu__header:first-child {
+    margin-top: 2px;
+  }
+}
+.settings-appearance__card,
+.settings-notifications,
+.settings-security__card,
+.settings-about__card {
+  @extend .settings-window__card;
+}
 
+.settings-window__cards-wrapper{
   & .setting-tile {
+    margin: 0 var(--sp-normal);
     margin-top: var(--sp-normal);
-    border-bottom: 1px solid var(--bg-surface-border);
     padding-bottom: 16px;
+    border-bottom: 1px solid var(--bg-surface-border);
+    &:last-child {
+      border-bottom: none;
+    }
   }
 }
 
-.set-notifications {
+.settings-notifications {
   &__not-supported {
     padding: 0 var(--sp-ultra-tight);
   }
 }
 
-.set-about {
+.settings-about {
   &__branding {
-    margin-top: var(--sp-extra-tight);
-    margin-bottom: var(--sp-normal);
+    padding: var(--sp-normal);
     display: flex;
-
+    
     & > div {
       margin: 0 var(--sp-loose);
     }
-
+    
   }
   &__btns {
-    margin: 0;
-    margin-top: var(--sp-normal);
-    & button:last-child {
-      margin: 0 var(--sp-tight)
+    & button {
+      margin-top: var(--sp-tight);
+      @include dir.side(margin, 0, var(--sp-tight));
     }
   }
-
+  
   &__credits {
-    margin-top: var(--sp-loose);
+    padding: 0 var(--sp-normal);
     & ul {
+      color: var(--tc-surface-low);
+      padding: var(--sp-normal);
       margin: var(--sp-extra-tight) 0;
     }
   }
index 501deedb22fba0d45f1ec6846af6452891097f05..987f23b8af3da75728f1ac175c8a3865d6e218a5 100644 (file)
@@ -7,14 +7,10 @@
 
   & .room-profile {
     padding: var(--sp-loose) var(--sp-extra-loose);
-  }
-
-  & .tabs {
-    box-shadow: inset 0 -1px 0 var(--bg-surface-border);
+  } 
 
-    &__content {
-      padding: 0 var(--sp-normal);
-    }
+  & .tabs__content {
+    padding: 0 var(--sp-normal);
   }
 
   &__cards-wrapper {
index c145502a7527cfa70c7efc6131aaa8365ff22ec2..d94044d189d8844f74fc30ec2efce2291f936686 100644 (file)
@@ -95,9 +95,10 @@ export function openProfileViewer(userId, roomId) {
   });
 }
 
-export function openSettings() {
+export function openSettings(tabText) {
   appDispatcher.dispatch({
     type: cons.actions.navigation.OPEN_SETTINGS,
+    tabText,
   });
 }
 
index 88de35fbb6962f93c42552c6d0007c28b9820250..06871e92f3b7f9af47d558cd1d6255c96217baf7 100644 (file)
@@ -129,12 +129,13 @@ class Navigation extends EventEmitter {
         this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId);
       },
       [cons.actions.navigation.OPEN_SETTINGS]: () => {
-        this.emit(cons.events.navigation.SETTINGS_OPENED);
+        this.emit(cons.events.navigation.SETTINGS_OPENED, action.tabText);
       },
       [cons.actions.navigation.OPEN_EMOJIBOARD]: () => {
         this.emit(
           cons.events.navigation.EMOJIBOARD_OPENED,
-          action.cords, action.requestEmojiCallback,
+          action.cords,
+          action.requestEmojiCallback,
         );
       },
       [cons.actions.navigation.OPEN_READRECEIPTS]: () => {