Redesign app/user settings (#404)

* 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>
This commit is contained in:
Ajay Bura 2022-03-21 09:46:11 +05:30 committed by GitHub
parent abb81b6390
commit 50bf90fada
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 369 additions and 295 deletions

View file

@ -74,7 +74,7 @@ Tabs.defaultProps = {
Tabs.propTypes = { Tabs.propTypes = {
items: PropTypes.arrayOf( items: PropTypes.arrayOf(
PropTypes.exact({ PropTypes.shape({
iconSrc: PropTypes.string, iconSrc: PropTypes.string,
text: PropTypes.string, text: PropTypes.string,
disabled: PropTypes.bool, disabled: PropTypes.bool,
@ -84,4 +84,4 @@ Tabs.propTypes = {
onSelect: PropTypes.func.isRequired, onSelect: PropTypes.func.isRequired,
}; };
export { Tabs as default }; export default Tabs;

View file

@ -72,7 +72,7 @@ function ProfileAvatarMenu() {
return ( return (
<SidebarAvatar <SidebarAvatar
onClick={openSettings} onClick={openSettings}
tooltip={profile.displayName} tooltip="Settings"
avatar={( avatar={(
<Avatar <Avatar
text={profile.displayName} text={profile.displayName}

View file

@ -1,35 +1,44 @@
import React, { useState, useEffect, useRef } from 'react'; import React, { useState, useEffect, useRef } from 'react';
import PropTypes from 'prop-types'; import PropTypes from 'prop-types';
import { twemojify } from '../../../util/twemojify';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import colorMXID from '../../../util/colorMXID'; import colorMXID from '../../../util/colorMXID';
import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
import ImageUpload from '../../molecules/image-upload/ImageUpload'; import ImageUpload from '../../molecules/image-upload/ImageUpload';
import Input from '../../atoms/input/Input'; import Input from '../../atoms/input/Input';
import PencilIC from '../../../../public/res/ic/outlined/pencil.svg';
import './ProfileEditor.scss'; import './ProfileEditor.scss';
// TODO Fix bug that prevents 'Save' button from enabling up until second changed. // TODO Fix bug that prevents 'Save' button from enabling up until second changed.
function ProfileEditor({ function ProfileEditor({ userId }) {
userId, const [isEditing, setIsEditing] = useState(false);
}) {
const mx = initMatrix.matrixClient; const mx = initMatrix.matrixClient;
const user = mx.getUser(mx.getUserId());
const displayNameRef = useRef(null); const displayNameRef = useRef(null);
const bgColor = colorMXID(userId); const [avatarSrc, setAvatarSrc] = useState(user.avatarUrl ? mx.mxcUrlToHttp(user.avatarUrl, 80, 80, 'crop') : null);
const [avatarSrc, setAvatarSrc] = useState(null); const [username, setUsername] = useState(user.displayName);
const [disabled, setDisabled] = useState(true); const [disabled, setDisabled] = useState(true);
let username = mx.getUser(mx.getUserId()).displayName;
useEffect(() => { useEffect(() => {
let isMounted = true;
mx.getProfileInfo(mx.getUserId()).then((info) => { mx.getProfileInfo(mx.getUserId()).then((info) => {
if (!isMounted) return;
setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null); setAvatarSrc(info.avatar_url ? mx.mxcUrlToHttp(info.avatar_url, 80, 80, 'crop') : null);
setUsername(info.displayname);
}); });
return () => {
isMounted = false;
};
}, [userId]); }, [userId]);
// Sets avatar URL and updates the avatar component in profile editor to reflect new upload const handleAvatarUpload = (url) => {
function handleAvatarUpload(url) {
if (url === null) { if (url === null) {
if (confirm('Are you sure you want to remove avatar?')) { if (confirm('Are you sure you want to remove avatar?')) {
mx.setAvatarUrl(''); mx.setAvatarUrl('');
@ -39,48 +48,72 @@ function ProfileEditor({
} }
mx.setAvatarUrl(url); mx.setAvatarUrl(url);
setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop')); setAvatarSrc(mx.mxcUrlToHttp(url, 80, 80, 'crop'));
} };
function saveDisplayName() { const saveDisplayName = () => {
const newDisplayName = displayNameRef.current.value; const newDisplayName = displayNameRef.current.value;
if (newDisplayName !== null && newDisplayName !== username) { if (newDisplayName !== null && newDisplayName !== username) {
mx.setDisplayName(newDisplayName); mx.setDisplayName(newDisplayName);
username = newDisplayName; setUsername(newDisplayName);
setDisabled(true); setDisabled(true);
setIsEditing(false);
} }
} };
function onDisplayNameInputChange() { const onDisplayNameInputChange = () => {
setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null); setDisabled(username === displayNameRef.current.value || displayNameRef.current.value == null);
} };
function cancelDisplayNameChanges() { const cancelDisplayNameChanges = () => {
displayNameRef.current.value = username; displayNameRef.current.value = username;
onDisplayNameInputChange(); onDisplayNameInputChange();
} setIsEditing(false);
};
return ( const renderForm = () => (
<form <form
className="profile-editor" className="profile-editor__form"
style={{ marginBottom: avatarSrc ? '24px' : '0' }}
onSubmit={(e) => { e.preventDefault(); saveDisplayName(); }} 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 <ImageUpload
text={username} text={username}
bgColor={bgColor} bgColor={colorMXID(userId)}
imageSrc={avatarSrc} imageSrc={avatarSrc}
onUpload={handleAvatarUpload} onUpload={handleAvatarUpload}
onRequestRemove={() => handleAvatarUpload(null)} onRequestRemove={() => handleAvatarUpload(null)}
/> />
<div className="profile-editor__input-wrapper"> {
<Input isEditing ? renderForm() : renderInfo()
label={`Display name of ${mx.getUserId()}`} }
onChange={onDisplayNameInputChange} </div>
value={mx.getUser(mx.getUserId()).displayName}
forwardRef={displayNameRef}
/>
<Button variant="primary" type="submit" disabled={disabled}>Save</Button>
<Button onClick={cancelDisplayNameChanges}>Cancel</Button>
</div>
</form>
); );
} }

View file

@ -1,28 +1,41 @@
@use '../../partials/dir'; @use '../../partials/dir';
@use '../../partials/flex';
.profile-editor { .profile-editor {
display: flex; display: flex;
align-items: flex-start; align-items: flex-end;
} }
.profile-editor__input-wrapper { .profile-editor__info,
flex: 1; .profile-editor__form {
min-width: 0; @extend .cp-fx__item-one;
margin-top: 10px; @include dir.side(margin, var(--sp-loose), 0);
display: flex; 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; flex-wrap: wrap;
align-items: flex-end;
& > .input-container { & > .input-container {
flex: 1; @extend .cp-fx__item-one;
} }
& > button { & > button {
height: 46px; height: 46px;
margin-top: var(--sp-normal); margin-top: var(--sp-normal);
}
& > * {
@include dir.side(margin, var(--sp-normal), 0); @include dir.side(margin, var(--sp-normal), 0);
} }
} }

View file

@ -18,7 +18,6 @@ function Windows() {
const [inviteUser, changeInviteUser] = useState({ const [inviteUser, changeInviteUser] = useState({
isOpen: false, roomId: undefined, term: undefined, isOpen: false, roomId: undefined, term: undefined,
}); });
const [settings, changeSettings] = useState(false);
function openInviteList() { function openInviteList() {
changeInviteList(true); changeInviteList(true);
@ -36,20 +35,15 @@ function Windows() {
searchTerm, searchTerm,
}); });
} }
function openSettings() {
changeSettings(true);
}
useEffect(() => { useEffect(() => {
navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.on(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); navigation.on(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); navigation.on(cons.events.navigation.INVITE_USER_OPENED, openInviteUser);
navigation.on(cons.events.navigation.SETTINGS_OPENED, openSettings);
return () => { return () => {
navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList); navigation.removeListener(cons.events.navigation.INVITE_LIST_OPENED, openInviteList);
navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms); navigation.removeListener(cons.events.navigation.PUBLIC_ROOMS_OPENED, openPublicRooms);
navigation.removeListener(cons.events.navigation.INVITE_USER_OPENED, openInviteUser); 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} searchTerm={inviteUser.searchTerm}
onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })} onRequestClose={() => changeInviteUser({ isOpen: false, roomId: undefined })}
/> />
<Settings <Settings />
isOpen={settings}
onRequestClose={() => changeSettings(false)}
/>
<SpaceSettings /> <SpaceSettings />
<SpaceManage /> <SpaceManage />
</> </>

View file

@ -1,10 +1,10 @@
import React, { useState } from 'react'; import React, { useState, useEffect } from 'react';
import PropTypes from 'prop-types';
import './Settings.scss'; import './Settings.scss';
import initMatrix from '../../../client/initMatrix'; import initMatrix from '../../../client/initMatrix';
import cons from '../../../client/state/cons'; import cons from '../../../client/state/cons';
import settings from '../../../client/state/settings'; import settings from '../../../client/state/settings';
import navigation from '../../../client/state/navigation';
import { import {
toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents, toggleSystemTheme, toggleMarkdown, toggleMembershipEvents, toggleNickAvatarEvents,
toggleNotifications, toggleNotificationSounds, toggleNotifications, toggleNotificationSounds,
@ -16,16 +16,17 @@ import Text from '../../atoms/text/Text';
import IconButton from '../../atoms/button/IconButton'; import IconButton from '../../atoms/button/IconButton';
import Button from '../../atoms/button/Button'; import Button from '../../atoms/button/Button';
import Toggle from '../../atoms/button/Toggle'; 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 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 SettingTile from '../../molecules/setting-tile/SettingTile';
import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys'; import ImportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ImportE2ERoomKeys';
import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys'; import ExportE2ERoomKeys from '../../molecules/import-export-e2e-room-keys/ExportE2ERoomKeys';
import ProfileEditor from '../profile-editor/ProfileEditor'; 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 SunIC from '../../../../public/res/ic/outlined/sun.svg';
import LockIC from '../../../../public/res/ic/outlined/lock.svg'; import LockIC from '../../../../public/res/ic/outlined/lock.svg';
import BellIC from '../../../../public/res/ic/outlined/bell.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'; 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() { function AppearanceSection() {
const [, updateState] = useState({}); const [, updateState] = useState({});
return ( return (
<div className="settings-content"> <div className="settings-appearance">
<SettingTile <div className="settings-appearance__card">
title="Follow system theme" <MenuHeader>Theme</MenuHeader>
options={( <SettingTile
<Toggle title="Follow system theme"
isActive={settings.useSystemTheme} options={(
onToggle={() => { toggleSystemTheme(); updateState({}); }} <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)}
/>
)}
/> />
); )}
} content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
})()} />
<SettingTile {!settings.useSystemTheme && (
title="Markdown formatting" <SettingTile
options={( title="Theme"
<Toggle content={(
isActive={settings.isMarkdown} <SegmentedControls
onToggle={() => { toggleMarkdown(); updateState({}); }} selected={settings.getThemeIndex()}
segments={[
{ text: 'Light' },
{ text: 'Silver' },
{ text: 'Dark' },
{ text: 'Butter' },
]}
onSelect={(index) => settings.setTheme(index)}
/>
)}
/> />
)} )}
content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>} </div>
/> <div className="settings-appearance__card">
<SettingTile <MenuHeader>Room messages</MenuHeader>
title="Hide membership events" <SettingTile
options={( title="Markdown formatting"
<Toggle options={(
isActive={settings.hideMembershipEvents} <Toggle
onToggle={() => { toggleMembershipEvents(); updateState({}); }} isActive={settings.isMarkdown}
/> onToggle={() => { toggleMarkdown(); updateState({}); }}
)} />
content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</Text>} )}
/> content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
<SettingTile />
title="Hide nick/avatar events" <SettingTile
options={( title="Hide membership events"
<Toggle options={(
isActive={settings.hideNickAvatarEvents} <Toggle
onToggle={() => { toggleNickAvatarEvents(); updateState({}); }} isActive={settings.hideMembershipEvents}
/> onToggle={() => { toggleMembershipEvents(); updateState({}); }}
)} />
content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>} )}
/> 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> </div>
); );
} }
@ -125,7 +115,7 @@ function NotificationsSection() {
const renderOptions = () => { const renderOptions = () => {
if (window.Notification === undefined) { 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') { if (permission === 'granted') {
@ -152,21 +142,22 @@ function NotificationsSection() {
}; };
return ( return (
<div className="set-notifications settings-content"> <div className="settings-notifications">
<MenuHeader>Notification & Sound</MenuHeader>
<SettingTile <SettingTile
title="Show desktop notifications" title="Desktop notification"
options={renderOptions()} 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 <SettingTile
title="Play notification sounds" title="Notification Sound"
options={( options={(
<Toggle <Toggle
isActive={settings.isNotificationSounds} isActive={settings.isNotificationSounds}
onToggle={() => { toggleNotificationSounds(); updateState({}); }} onToggle={() => { toggleNotificationSounds(); updateState({}); }}
/> />
)} )}
content={<Text variant="b3">Play a sound when new messages arrive.</Text>} content={<Text variant="b3">Play sound when new messages arrive.</Text>}
/> />
</div> </div>
); );
@ -174,153 +165,173 @@ function NotificationsSection() {
function SecuritySection() { function SecuritySection() {
return ( return (
<div className="set-security settings-content"> <div className="settings-security">
<SettingTile <div className="settings-security__card">
title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`} <MenuHeader>Device Info</MenuHeader>
/> <SettingTile
<SettingTile title={`Device ID: ${initMatrix.matrixClient.getDeviceId()}`}
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={`Device key: ${initMatrix.matrixClient.getDeviceEd25519Key().match(/.{1,4}/g).join(' ')}`}
<SettingTile content={<Text variant="b3">Use this device ID-key combo to verify or manage this session from Element client.</Text>}
title="Export E2E room keys" />
content={( </div>
<> <div className="settings-security__card">
<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> <MenuHeader>Encryption</MenuHeader>
<ExportE2ERoomKeys /> <SettingTile
</> title="Export E2E room keys"
)} content={(
/> <>
<SettingTile <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>
title="Import E2E room keys" <ExportE2ERoomKeys />
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 /> <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> </div>
); );
} }
function AboutSection() { function AboutSection() {
return ( return (
<div className="settings-content set__about"> <div className="settings-about">
<div className="set-about__branding"> <div className="settings-about__card">
<img width="60" height="60" src={CinnySVG} alt="Cinny logo" /> <MenuHeader>Application</MenuHeader>
<div> <div className="settings-about__branding">
<Text variant="h2" weight="medium"> <img width="60" height="60" src={CinnySVG} alt="Cinny logo" />
Cinny <div>
<span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span> <Text variant="h2" weight="medium">
</Text> Cinny
<Text>Yet another matrix client</Text> <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"> <div className="settings-about__btns">
<Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button> <Button onClick={() => window.open('https://github.com/ajbura/cinny')}>Source code</Button>
<Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button> <Button onClick={() => window.open('https://cinny.in/#sponsor')}>Support</Button>
</div>
</div> </div>
</div> </div>
</div> </div>
<div className="set-about__credits"> <div className="settings-about__card">
<Text variant="s1" weight="medium">Credits</Text> <MenuHeader>Credits</MenuHeader>
<ul> <div className="settings-about__credits">
<li> <ul>
{/* eslint-disable-next-line react/jsx-one-expression-per-line */ } <li>
<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> {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
</li> <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 */ } <li>
<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> {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
</li> <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 */ } <li>
<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> {/* eslint-disable-next-line react/jsx-one-expression-per-line */ }
</li> <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>
</ul> </li>
</ul>
</div>
</div> </div>
</div> </div>
); );
} }
function Settings({ isOpen, onRequestClose }) { const tabText = {
const settingSections = [{ APPEARANCE: 'Appearance',
name: 'General', NOTIFICATIONS: 'Notifications',
iconSrc: SettingsIC, SECURITY: 'Security',
render() { ABOUT: 'About',
return <GeneralSection />; };
}, const tabItems = [{
}, { text: tabText.APPEARANCE,
name: 'Appearance', iconSrc: SunIC,
iconSrc: SunIC, disabled: false,
render() { render: () => <AppearanceSection />,
return <AppearanceSection />; }, {
}, text: tabText.NOTIFICATIONS,
}, { iconSrc: BellIC,
name: 'Notifications', disabled: false,
iconSrc: BellIC, render: () => <NotificationsSection />,
render() { }, {
return <NotificationsSection />; text: tabText.SECURITY,
}, iconSrc: LockIC,
}, { disabled: false,
name: 'Security & Privacy', render: () => <SecuritySection />,
iconSrc: LockIC, }, {
render() { text: tabText.ABOUT,
return <SecuritySection />; iconSrc: InfoIC,
}, disabled: false,
}, { render: () => <AboutSection />,
name: 'Help & About', }];
iconSrc: InfoIC,
render() {
return <AboutSection />;
},
}];
const [selectedSection, setSelectedSection] = useState(settingSections[0]);
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 = () => { const handleLogout = () => {
if (confirm('Confirm logout')) logout(); if (confirm('Confirm logout')) logout();
}; };
return ( return (
<PopupWindow <PopupWindow
className="settings-window"
isOpen={isOpen} isOpen={isOpen}
onRequestClose={onRequestClose} className="settings-window"
title="Settings" title={<Text variant="s1" weight="medium" primary>Settings</Text>}
contentTitle={selectedSection.name} contentOptions={(
drawer={(
<> <>
{ <Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
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}
>
Logout 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> </PopupWindow>
); );
} }
Settings.propTypes = {
isOpen: PropTypes.bool.isRequired,
onRequestClose: PropTypes.func.isRequired,
};
export default Settings; export default Settings;

View file

@ -2,40 +2,67 @@
@use '../../partials/dir'; @use '../../partials/dir';
.settings-window { .settings-window {
& .pw__drawer__content { & .pw {
@extend .cp-fx__column; background-color: var(--bg-surface-low);
min-height: 100%; }
padding-bottom: var(--sp-extra-tight);
& > .pw-content-selector:last-child { .header .btn-danger {
margin-top: auto; margin: 0 var(--sp-tight);
box-shadow: none;
}
& .profile-editor {
padding: var(--sp-loose) var(--sp-extra-loose);
}
& .tabs__content {
padding: 0 var(--sp-normal);
}
&__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;
& > .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);
padding-bottom: 16px;
border-bottom: 1px solid var(--bg-surface-border);
&:last-child {
border-bottom: none;
} }
} }
& .pw__content-container {
min-height: 100%;
}
} }
.settings-content { .settings-notifications {
@include dir.side(margin, var(--sp-normal), var(--sp-extra-tight));
& .setting-tile {
margin-top: var(--sp-normal);
border-bottom: 1px solid var(--bg-surface-border);
padding-bottom: 16px;
}
}
.set-notifications {
&__not-supported { &__not-supported {
padding: 0 var(--sp-ultra-tight); padding: 0 var(--sp-ultra-tight);
} }
} }
.set-about { .settings-about {
&__branding { &__branding {
margin-top: var(--sp-extra-tight); padding: var(--sp-normal);
margin-bottom: var(--sp-normal);
display: flex; display: flex;
& > div { & > div {
@ -44,16 +71,17 @@
} }
&__btns { &__btns {
margin: 0; & button {
margin-top: var(--sp-normal); margin-top: var(--sp-tight);
& button:last-child { @include dir.side(margin, 0, var(--sp-tight));
margin: 0 var(--sp-tight)
} }
} }
&__credits { &__credits {
margin-top: var(--sp-loose); padding: 0 var(--sp-normal);
& ul { & ul {
color: var(--tc-surface-low);
padding: var(--sp-normal);
margin: var(--sp-extra-tight) 0; margin: var(--sp-extra-tight) 0;
} }
} }

View file

@ -9,12 +9,8 @@
padding: var(--sp-loose) var(--sp-extra-loose); padding: var(--sp-loose) var(--sp-extra-loose);
} }
& .tabs { & .tabs__content {
box-shadow: inset 0 -1px 0 var(--bg-surface-border); padding: 0 var(--sp-normal);
&__content {
padding: 0 var(--sp-normal);
}
} }
&__cards-wrapper { &__cards-wrapper {

View file

@ -95,9 +95,10 @@ export function openProfileViewer(userId, roomId) {
}); });
} }
export function openSettings() { export function openSettings(tabText) {
appDispatcher.dispatch({ appDispatcher.dispatch({
type: cons.actions.navigation.OPEN_SETTINGS, type: cons.actions.navigation.OPEN_SETTINGS,
tabText,
}); });
} }

View file

@ -129,12 +129,13 @@ class Navigation extends EventEmitter {
this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId); this.emit(cons.events.navigation.PROFILE_VIEWER_OPENED, action.userId, action.roomId);
}, },
[cons.actions.navigation.OPEN_SETTINGS]: () => { [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]: () => { [cons.actions.navigation.OPEN_EMOJIBOARD]: () => {
this.emit( this.emit(
cons.events.navigation.EMOJIBOARD_OPENED, cons.events.navigation.EMOJIBOARD_OPENED,
action.cords, action.requestEmojiCallback, action.cords,
action.requestEmojiCallback,
); );
}, },
[cons.actions.navigation.OPEN_READRECEIPTS]: () => { [cons.actions.navigation.OPEN_READRECEIPTS]: () => {