diff --git a/src/app/atoms/tabs/Tabs.jsx b/src/app/atoms/tabs/Tabs.jsx index 5426cf3..39800ce 100644 --- a/src/app/atoms/tabs/Tabs.jsx +++ b/src/app/atoms/tabs/Tabs.jsx @@ -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; diff --git a/src/app/organisms/navigation/SideBar.jsx b/src/app/organisms/navigation/SideBar.jsx index 4a30522..54723bd 100644 --- a/src/app/organisms/navigation/SideBar.jsx +++ b/src/app/organisms/navigation/SideBar.jsx @@ -72,7 +72,7 @@ function ProfileAvatarMenu() { return ( { + 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 = () => (
{ e.preventDefault(); saveDisplayName(); }} > + + + +
+ ); + + const renderInfo = () => ( +
+
+ {twemojify(username)} + setIsEditing(true)} + /> +
+ {mx.getUserId()} +
+ ); + + return ( +
handleAvatarUpload(null)} /> -
- - - -
- + { + isEditing ? renderForm() : renderInfo() + } +
); } diff --git a/src/app/organisms/profile-editor/ProfileEditor.scss b/src/app/organisms/profile-editor/ProfileEditor.scss index 9f95e5b..2e2ef91 100644 --- a/src/app/organisms/profile-editor/ProfileEditor.scss +++ b/src/app/organisms/profile-editor/ProfileEditor.scss @@ -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 diff --git a/src/app/organisms/pw/Windows.jsx b/src/app/organisms/pw/Windows.jsx index ae2bc1a..ba80f13 100644 --- a/src/app/organisms/pw/Windows.jsx +++ b/src/app/organisms/pw/Windows.jsx @@ -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 })} /> - changeSettings(false)} - /> + diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index 87f2766..84013cc 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -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 ( -
- - )} - /> -
- ); -} - function AppearanceSection() { const [, updateState] = useState({}); return ( -
- { toggleSystemTheme(); updateState({}); }} - /> - )} - content={Use light or dark mode based on the system's settings.} - /> - {(() => { - if (!settings.useSystemTheme) { - return ( - settings.setTheme(index)} - /> - )} +
+
+ Theme + { toggleSystemTheme(); updateState({}); }} /> - ); - } - })()} - { toggleMarkdown(); updateState({}); }} + )} + content={Use light or dark mode based on the system settings.} + /> + {!settings.useSystemTheme && ( + settings.setTheme(index)} + /> + )} /> )} - content={Format messages with markdown syntax before sending.} - /> - { toggleMembershipEvents(); updateState({}); }} - /> - )} - content={Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)} - /> - { toggleNickAvatarEvents(); updateState({}); }} - /> - )} - content={Hide nick and avatar change messages from room timeline.} - /> +
+
+ Room messages + { toggleMarkdown(); updateState({}); }} + /> + )} + content={Format messages with markdown syntax before sending.} + /> + { toggleMembershipEvents(); updateState({}); }} + /> + )} + content={Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)} + /> + { toggleNickAvatarEvents(); updateState({}); }} + /> + )} + content={Hide nick and avatar change messages from room timeline.} + /> +
); } @@ -125,7 +115,7 @@ function NotificationsSection() { const renderOptions = () => { if (window.Notification === undefined) { - return Not supported in this browser.; + return Not supported in this browser.; } if (permission === 'granted') { @@ -152,21 +142,22 @@ function NotificationsSection() { }; return ( -
+
+ Notification & Sound Show notifications when new messages arrive.} + content={Show desktop notification when new messages arrive.} /> { toggleNotificationSounds(); updateState({}); }} /> )} - content={Play a sound when new messages arrive.} + content={Play sound when new messages arrive.} />
); @@ -174,153 +165,173 @@ function NotificationsSection() { function SecuritySection() { return ( -
- - Use this device ID-key combo to verify or manage this session from Element client.} - /> - - 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. - - - )} - /> - - {'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.'} - - - )} - /> +
+
+ Device Info + + Use this device ID-key combo to verify or manage this session from Element client.} + /> +
+
+ Encryption + + 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. + + + )} + /> + + {'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.'} + + + )} + /> +
); } function AboutSection() { return ( -
-
- Cinny logo -
- - Cinny - {`v${cons.version}`} - - Yet another matrix client +
+
+ Application +
+ Cinny logo +
+ + Cinny + {`v${cons.version}`} + + Yet another matrix client -
- - +
+ + +
-
- Credits - +
+ Credits +
+ +
); } -function Settings({ isOpen, onRequestClose }) { - const settingSections = [{ - name: 'General', - iconSrc: SettingsIC, - render() { - return ; - }, - }, { - name: 'Appearance', - iconSrc: SunIC, - render() { - return ; - }, - }, { - name: 'Notifications', - iconSrc: BellIC, - render() { - return ; - }, - }, { - name: 'Security & Privacy', - iconSrc: LockIC, - render() { - return ; - }, - }, { - name: 'Help & About', - iconSrc: InfoIC, - render() { - return ; - }, - }]; - 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: () => , +}, { + text: tabText.NOTIFICATIONS, + iconSrc: BellIC, + disabled: false, + render: () => , +}, { + text: tabText.SECURITY, + iconSrc: LockIC, + disabled: false, + render: () => , +}, { + text: tabText.ABOUT, + iconSrc: InfoIC, + disabled: false, + render: () => , +}]; +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 ( Settings} + contentOptions={( <> - { - settingSections.map((section) => ( - setSelectedSection(section)} - iconSrc={section.iconSrc} - > - {section.name} - - )) - } - + + )} - contentOptions={} + onRequestClose={requestClose} > - {selectedSection.render()} + {isOpen && ( +
+ + tab.text === selectedTab.text)} + onSelect={handleTabChange} + /> +
+ { selectedTab.render() } +
+
+ )}
); } -Settings.propTypes = { - isOpen: PropTypes.bool.isRequired, - onRequestClose: PropTypes.func.isRequired, -}; - export default Settings; diff --git a/src/app/organisms/settings/Settings.scss b/src/app/organisms/settings/Settings.scss index 5445122..2d086c6 100644 --- a/src/app/organisms/settings/Settings.scss +++ b/src/app/organisms/settings/Settings.scss @@ -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; } } diff --git a/src/app/organisms/space-settings/SpaceSettings.scss b/src/app/organisms/space-settings/SpaceSettings.scss index 501deed..987f23b 100644 --- a/src/app/organisms/space-settings/SpaceSettings.scss +++ b/src/app/organisms/space-settings/SpaceSettings.scss @@ -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 { diff --git a/src/client/action/navigation.js b/src/client/action/navigation.js index c145502..d94044d 100644 --- a/src/client/action/navigation.js +++ b/src/client/action/navigation.js @@ -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, }); } diff --git a/src/client/state/navigation.js b/src/client/state/navigation.js index 88de35f..06871e9 100644 --- a/src/client/state/navigation.js +++ b/src/client/state/navigation.js @@ -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]: () => {