import React, { useState, useEffect } from 'react'; import { Input, toRem } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; import './Settings.scss'; import { clearCacheAndReload, logoutClient } from '../../../client/initMatrix'; import cons from '../../../client/state/cons'; import settings from '../../../client/state/settings'; import navigation from '../../../client/state/navigation'; import { toggleSystemTheme } from '../../../client/action/settings'; import { usePermissionState } from '../../hooks/usePermission'; 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 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 { ImagePackUser, ImagePackGlobal } from '../../molecules/image-pack/ImagePack'; import GlobalNotification from '../../molecules/global-notification/GlobalNotification'; import KeywordNotification from '../../molecules/global-notification/KeywordNotification'; import IgnoreUserList from '../../molecules/global-notification/IgnoreUserList'; import ProfileEditor from '../profile-editor/ProfileEditor'; import CrossSigning from './CrossSigning'; import KeyBackup from './KeyBackup'; import DeviceManage from './DeviceManage'; import SunIC from '../../../../public/res/ic/outlined/sun.svg'; import EmojiIC from '../../../../public/res/ic/outlined/emoji.svg'; import LockIC from '../../../../public/res/ic/outlined/lock.svg'; import BellIC from '../../../../public/res/ic/outlined/bell.svg'; import InfoIC from '../../../../public/res/ic/outlined/info.svg'; import PowerIC from '../../../../public/res/ic/outlined/power.svg'; import CrossIC from '../../../../public/res/ic/outlined/cross.svg'; import CinnySVG from '../../../../public/res/svg/cinny.svg'; import { confirmDialog } from '../../molecules/confirm-dialog/ConfirmDialog'; import { useSetting } from '../../state/hooks/settings'; import { settingsAtom } from '../../state/settings'; import { isMacOS } from '../../utils/user-agent'; import { KeySymbol } from '../../utils/key-symbol'; import { useMatrixClient } from '../../hooks/useMatrixClient'; function AppearanceSection() { const [, updateState] = useState({}); const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout'); const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing'); const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom'); const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [hideMembershipEvents, setHideMembershipEvents] = useSetting( settingsAtom, 'hideMembershipEvents' ); const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting( settingsAtom, 'hideNickAvatarEvents' ); const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview'); const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview'); const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents'); const spacings = ['0', '100', '200', '300', '400', '500']; const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`); const handleZoomChange = (evt) => { setCurrentZoom(evt.target.value); }; const handleZoomEnter = (evt) => { if (isKeyHotkey('escape', evt)) { evt.stopPropagation(); setCurrentZoom(pageZoom); } if (isKeyHotkey('enter', evt)) { const newZoom = parseInt(evt.target.value, 10); if (Number.isNaN(newZoom)) return; const safeZoom = Math.max(Math.min(newZoom, 150), 75); setPageZoom(safeZoom); setCurrentZoom(safeZoom); } }; return (
Theme { toggleSystemTheme(); updateState({}); }} /> } content={Use light or dark mode based on the system settings.} /> { if (settings.useSystemTheme) toggleSystemTheme(); settings.setTheme(index); updateState({}); }} /> } /> setTwitterEmoji(!twitterEmoji)} /> } content={Use Twitter emoji instead of system emoji.} /> %} /> } content={ Change page zoom to scale user interface between 75% to 150%. Default: 100% } />
Room messages setMessageLayout(index)} /> } /> s === messageSpacing)} segments={[ { text: 'No' }, { text: 'XXS' }, { text: 'XS' }, { text: 'S' }, { text: 'M' }, { text: 'L' }, ]} onSelect={(index) => { setMessageSpacing(spacings[index]); }} /> } /> setEnterForNewline(!enterForNewline)} /> } content={ {`Use ${ isMacOS() ? KeySymbol.Command : 'Ctrl' } + ENTER to send message and ENTER for newline.`} } /> setIsMarkdown(!isMarkdown)} />} content={Format messages with markdown syntax before sending.} /> setHideMembershipEvents(!hideMembershipEvents)} /> } content={ Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban) } /> setHideNickAvatarEvents(!hideNickAvatarEvents)} /> } content={ Hide nick and avatar change messages from room timeline. } /> setMediaAutoLoad(!mediaAutoLoad)} /> } content={ Prevent images and videos from auto loading to save bandwidth. } /> setUrlPreview(!urlPreview)} />} content={Show url preview for link in messages.} /> setEncUrlPreview(!encUrlPreview)} /> } content={Show url preview for link in encrypted messages.} /> setShowHiddenEvents(!showHiddenEvents)} /> } content={Show hidden state and message events.} />
); } function NotificationsSection() { const notifPermission = usePermissionState( 'notifications', window.Notification?.permission ?? 'denied' ); const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications'); const [isNotificationSounds, setIsNotificationSounds] = useSetting( settingsAtom, 'isNotificationSounds' ); const renderOptions = () => { if (window.Notification === undefined) { return ( Not supported in this browser. ); } if (notifPermission === 'denied') { return Permission Denied; } if (notifPermission === 'granted') { return ( { setShowNotifications(!showNotifications); }} /> ); } return ( ); }; return ( <>
Notification & Sound Show desktop notification when new messages arrive.} /> setIsNotificationSounds(!isNotificationSounds)} /> } content={Play sound when new messages arrive.} />
); } function EmojiSection() { return ( <>
); } function SecuritySection() { return (
Cross signing and backup
Export/Import encryption keys 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() { const mx = useMatrixClient(); return (
Application
Cinny logo
Cinny {`v${cons.version}`} Yet another matrix client
Credits
); } export const tabText = { APPEARANCE: 'Appearance', NOTIFICATIONS: 'Notifications', EMOJI: 'Emoji', SECURITY: 'Security', ABOUT: 'About', }; const tabItems = [ { text: tabText.APPEARANCE, iconSrc: SunIC, disabled: false, render: () => , }, { text: tabText.NOTIFICATIONS, iconSrc: BellIC, disabled: false, render: () => , }, { text: tabText.EMOJI, iconSrc: EmojiIC, 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 mx = useMatrixClient(); const handleTabChange = (tabItem) => setSelectedTab(tabItem); const handleLogout = async () => { if ( await confirmDialog( 'Logout', 'Are you sure that you want to logout your session?', 'Logout', 'danger' ) ) { logoutClient(mx); } }; return ( Settings } contentOptions={ <> } onRequestClose={requestClose} > {isOpen && (
tab.text === selectedTab.text)} onSelect={handleTabChange} />
{selectedTab.render()}
)}
); } export default Settings;