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:
parent
abb81b6390
commit
50bf90fada
10 changed files with 369 additions and 295 deletions
|
@ -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;
|
||||
|
|
|
@ -72,7 +72,7 @@ function ProfileAvatarMenu() {
|
|||
return (
|
||||
<SidebarAvatar
|
||||
onClick={openSettings}
|
||||
tooltip={profile.displayName}
|
||||
tooltip="Settings"
|
||||
avatar={(
|
||||
<Avatar
|
||||
text={profile.displayName}
|
||||
|
|
|
@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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 />
|
||||
</>
|
||||
|
|
|
@ -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">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">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 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 a 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;
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
|
||||
&__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 {
|
||||
@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 {
|
||||
.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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -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]: () => {
|
||||
|
|
Loading…
Reference in a new issue