diff --git a/src/app/organisms/settings/Settings.jsx b/src/app/organisms/settings/Settings.jsx index 779931d..6329a57 100644 --- a/src/app/organisms/settings/Settings.jsx +++ b/src/app/organisms/settings/Settings.jsx @@ -1,13 +1,13 @@ 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 { toggleSystemTheme } from '../../../client/action/settings'; import { usePermissionState } from '../../hooks/usePermission'; import Text from '../../atoms/text/Text'; @@ -55,14 +55,41 @@ function AppearanceSection() { 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 [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 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 (
@@ -70,17 +97,20 @@ function AppearanceSection() { Theme { toggleSystemTheme(); updateState({}); }} + onToggle={() => { + toggleSystemTheme(); + updateState({}); + }} /> - )} + } content={Use light or dark mode based on the system settings.} /> - )} + } /> setTwitterEmoji(!twitterEmoji)} - /> - )} + options={ + 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 @@ -114,113 +164,106 @@ function AppearanceSection() { title="Message Layout" content={ setMessageLayout(index)} - /> + selected={messageLayout} + segments={[{ text: 'Modern' }, { text: 'Compact' }, { text: 'Bubble' }]} + onSelect={(index) => setMessageLayout(index)} + /> } /> s === messageSpacing)} - segments={[ - { text: 'No' }, - { text: 'XXS' }, - { text: 'XS' }, - { text: 'S' }, - { text: 'M' }, - { text: 'L' }, - ]} - onSelect={(index) => { - setMessageSpacing(spacings[index]) - }} - /> + selected={spacings.findIndex((s) => s === messageSpacing)} + segments={[ + { text: 'No' }, + { text: 'XXS' }, + { text: 'XS' }, + { text: 'S' }, + { text: 'M' }, + { text: 'L' }, + ]} + onSelect={(index) => { + setMessageSpacing(spacings[index]); + }} + /> } /> setEnterForNewline(!enterForNewline) } + onToggle={() => setEnterForNewline(!enterForNewline)} /> - )} - content={{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}} + } + content={ + {`Use ${ + isMacOS() ? KeySymbol.Command : 'Ctrl' + } + ENTER to send message and ENTER for newline.`} + } /> setIsMarkdown(!isMarkdown) } - /> - )} + options={ 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)} + } + 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.} + } + content={ + Hide nick and avatar change messages from room timeline. + } /> setMediaAutoLoad(!mediaAutoLoad)} - /> - )} - content={Prevent images and videos from auto loading to save bandwidth.} + options={ + setMediaAutoLoad(!mediaAutoLoad)} /> + } + content={ + Prevent images and videos from auto loading to save bandwidth. + } /> setUrlPreview(!urlPreview)} - /> - )} + options={ setUrlPreview(!urlPreview)} />} content={Show url preview for link in messages.} /> setEncUrlPreview(!encUrlPreview)} - /> - )} + options={ + setEncUrlPreview(!encUrlPreview)} /> + } content={Show url preview for link in encrypted messages.} /> setShowHiddenEvents(!showHiddenEvents)} /> - )} + } content={Show hidden state and message events.} />
@@ -229,19 +272,29 @@ function AppearanceSection() { } function NotificationsSection() { - const notifPermission = usePermissionState('notifications', window.Notification?.permission ?? "denied"); - const [showNotifications, setShowNotifications] = useSetting(settingsAtom, 'showNotifications') - const [isNotificationSounds, setIsNotificationSounds] = useSetting(settingsAtom, 'isNotificationSounds') + 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.; + return ( + + Not supported in this browser. + + ); } if (notifPermission === 'denied') { - return Permission Denied + return Permission Denied; } - + if (notifPermission === 'granted') { return ( window.Notification.requestPermission().then(() => { - setShowNotifications(window.Notification?.permission === 'granted'); - })} + onClick={() => + window.Notification.requestPermission().then(() => { + setShowNotifications(window.Notification?.permission === 'granted'); + }) + } > Request permission @@ -276,12 +331,12 @@ function NotificationsSection() { /> setIsNotificationSounds(!isNotificationSounds)} /> - )} + } content={Play sound when new messages arrive.} /> @@ -295,8 +350,12 @@ function NotificationsSection() { function EmojiSection() { return ( <> -
-
+
+ +
+
+ +
); } @@ -314,21 +373,29 @@ function SecuritySection() { 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. + + 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.'} + + { + "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." + } + - )} + } /> @@ -337,7 +404,7 @@ function SecuritySection() { function AboutSection() { const mx = useMatrixClient(); - + return (
@@ -347,14 +414,21 @@ function AboutSection() {
Cinny - {`v${cons.version}`} + {`v${cons.version}`} Yet another matrix client
- + - +
@@ -364,20 +438,104 @@ function AboutSection() {
@@ -393,32 +551,38 @@ export const tabText = { 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: () => , -}]; +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); @@ -447,7 +611,14 @@ function Settings() { const handleTabChange = (tabItem) => setSelectedTab(tabItem); const handleLogout = async () => { - if (await confirmDialog('Logout', 'Are you sure that you want to logout your session?', 'Logout', 'danger')) { + if ( + await confirmDialog( + 'Logout', + 'Are you sure that you want to logout your session?', + 'Logout', + 'danger' + ) + ) { logoutClient(mx); } }; @@ -456,15 +627,19 @@ function Settings() { Settings} - contentOptions={( + title={ + + Settings + + } + contentOptions={ <> - )} + } onRequestClose={requestClose} > {isOpen && ( @@ -475,9 +650,7 @@ function Settings() { defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)} onSelect={handleTabChange} /> -
- { selectedTab.render() } -
+
{selectedTab.render()}
)} diff --git a/src/app/pages/client/ClientNonUIFeatures.tsx b/src/app/pages/client/ClientNonUIFeatures.tsx index 5f557aa..845fceb 100644 --- a/src/app/pages/client/ClientNonUIFeatures.tsx +++ b/src/app/pages/client/ClientNonUIFeatures.tsx @@ -26,6 +26,30 @@ import { getMxIdLocalPart } from '../../utils/matrix'; import { useSelectedRoom } from '../../hooks/router/useSelectedRoom'; import { useInboxNotificationsSelected } from '../../hooks/router/useInbox'; +function SystemEmojiFeature() { + const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); + + if (twitterEmoji) { + document.documentElement.style.setProperty('--font-emoji', 'Twemoji'); + } else { + document.documentElement.style.setProperty('--font-emoji', 'Twemoji_DISABLED'); + } + + return null; +} + +function PageZoomFeature() { + const [pageZoom] = useSetting(settingsAtom, 'pageZoom'); + + if (pageZoom === 100) { + document.documentElement.style.removeProperty('font-size'); + } else { + document.documentElement.style.setProperty('font-size', `calc(1em * ${pageZoom / 100})`); + } + + return null; +} + function FaviconUpdater() { const roomToUnread = useAtomValue(roomToUnreadAtom); @@ -233,6 +257,8 @@ type ClientNonUIFeaturesProps = { export function ClientNonUIFeatures({ children }: ClientNonUIFeaturesProps) { return ( <> + + diff --git a/src/app/pages/client/ClientRoot.tsx b/src/app/pages/client/ClientRoot.tsx index a590e0b..f30bf63 100644 --- a/src/app/pages/client/ClientRoot.tsx +++ b/src/app/pages/client/ClientRoot.tsx @@ -32,25 +32,11 @@ import { SpecVersions } from './SpecVersions'; import Windows from '../../organisms/pw/Windows'; import Dialogs from '../../organisms/pw/Dialogs'; import ReusableContextMenu from '../../atoms/context-menu/ReusableContextMenu'; -import { useSetting } from '../../state/hooks/settings'; -import { settingsAtom } from '../../state/settings'; import { AsyncStatus, useAsyncCallback } from '../../hooks/useAsyncCallback'; import { useSyncState } from '../../hooks/useSyncState'; import { stopPropagation } from '../../utils/keyboard'; import { SyncStatus } from './SyncStatus'; -function SystemEmojiFeature() { - const [twitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); - - if (twitterEmoji) { - document.documentElement.style.setProperty('--font-emoji', 'Twemoji'); - } else { - document.documentElement.style.setProperty('--font-emoji', 'Twemoji_DISABLED'); - } - - return null; -} - function ClientRootLoading() { return ( @@ -198,7 +184,7 @@ export function ClientRoot({ children }: ClientRootProps) { {startState.status === AsyncStatus.Error && ( {`Failed to load. ${startState.error.message}`} )} -