Add setting for page zoom (#1835)
authorAjay Bura <32841439+ajbura@users.noreply.github.com>
Tue, 23 Jul 2024 13:52:53 +0000 (19:22 +0530)
committerGitHub <noreply@github.com>
Tue, 23 Jul 2024 13:52:53 +0000 (23:52 +1000)
* add setting for page zoom

* parse integer in zoom change listener

* fix zoom input width

* fix null gets saved as page zoom

src/app/organisms/settings/Settings.jsx
src/app/pages/client/ClientNonUIFeatures.tsx
src/app/pages/client/ClientRoot.tsx
src/app/state/settings.ts

index 779931df3f7463833a853499601b70f43c43804d..6329a57fe0b4e4477f7699f6834ea2ce6a4a09f3 100644 (file)
@@ -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 (
     <div className="settings-appearance">
@@ -70,17 +97,20 @@ function AppearanceSection() {
         <MenuHeader>Theme</MenuHeader>
         <SettingTile
           title="Follow system theme"
-          options={(
+          options={
             <Toggle
               isActive={settings.useSystemTheme}
-              onToggle={() => { toggleSystemTheme(); updateState({}); }}
+              onToggle={() => {
+                toggleSystemTheme();
+                updateState({});
+              }}
             />
-          )}
+          }
           content={<Text variant="b3">Use light or dark mode based on the system settings.</Text>}
         />
         <SettingTile
           title="Theme"
-          content={(
+          content={
             <SegmentedControls
               selected={settings.useSystemTheme ? -1 : settings.getThemeIndex()}
               segments={[
@@ -95,18 +125,38 @@ function AppearanceSection() {
                 updateState({});
               }}
             />
-        )}
+          }
         />
         <SettingTile
           title="Use Twitter Emoji"
-          options={(
-            <Toggle
-              isActive={twitterEmoji}
-              onToggle={() => setTwitterEmoji(!twitterEmoji)}
-            />
-          )}
+          options={
+            <Toggle isActive={twitterEmoji} onToggle={() => setTwitterEmoji(!twitterEmoji)} />
+          }
           content={<Text variant="b3">Use Twitter emoji instead of system emoji.</Text>}
         />
+        <SettingTile
+          title="Page Zoom"
+          options={
+            <Input
+              style={{ width: toRem(150) }}
+              variant={pageZoom === parseInt(currentZoom, 10) ? 'Background' : 'Primary'}
+              size="400"
+              type="number"
+              min="75"
+              max="150"
+              value={currentZoom}
+              onChange={handleZoomChange}
+              onKeyDown={handleZoomEnter}
+              outlined
+              after={<Text variant="b2">%</Text>}
+            />
+          }
+          content={
+            <Text variant="b3">
+              Change page zoom to scale user interface between 75% to 150%. Default: 100%
+            </Text>
+          }
+        />
       </div>
       <div className="settings-appearance__card">
         <MenuHeader>Room messages</MenuHeader>
@@ -114,113 +164,106 @@ function AppearanceSection() {
           title="Message Layout"
           content={
             <SegmentedControls
-            selected={messageLayout}
-            segments={[
-              { text: 'Modern' },
-              { text: 'Compact' },
-              { text: 'Bubble' },
-            ]}
-            onSelect={(index) => setMessageLayout(index)}
-          />
+              selected={messageLayout}
+              segments={[{ text: 'Modern' }, { text: 'Compact' }, { text: 'Bubble' }]}
+              onSelect={(index) => setMessageLayout(index)}
+            />
           }
         />
         <SettingTile
           title="Message Spacing"
           content={
             <SegmentedControls
-            selected={spacings.findIndex((s) => 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]);
+              }}
+            />
           }
         />
         <SettingTile
           title="Use ENTER for Newline"
-          options={(
+          options={
             <Toggle
               isActive={enterForNewline}
-              onToggle={() => setEnterForNewline(!enterForNewline) }
+              onToggle={() => setEnterForNewline(!enterForNewline)}
             />
-          )}
-          content={<Text variant="b3">{`Use ${isMacOS() ? KeySymbol.Command : 'Ctrl'} + ENTER to send message and ENTER for newline.`}</Text>}
+          }
+          content={
+            <Text variant="b3">{`Use ${
+              isMacOS() ? KeySymbol.Command : 'Ctrl'
+            } + ENTER to send message and ENTER for newline.`}</Text>
+          }
         />
         <SettingTile
           title="Markdown formatting"
-          options={(
-            <Toggle
-              isActive={isMarkdown}
-              onToggle={() => setIsMarkdown(!isMarkdown) }
-            />
-          )}
+          options={<Toggle isActive={isMarkdown} onToggle={() => setIsMarkdown(!isMarkdown)} />}
           content={<Text variant="b3">Format messages with markdown syntax before sending.</Text>}
         />
         <SettingTile
           title="Hide membership events"
-          options={(
+          options={
             <Toggle
               isActive={hideMembershipEvents}
               onToggle={() => setHideMembershipEvents(!hideMembershipEvents)}
             />
-          )}
-          content={<Text variant="b3">Hide membership change messages from room timeline. (Join, Leave, Invite, Kick and Ban)</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={(
+          options={
             <Toggle
               isActive={hideNickAvatarEvents}
               onToggle={() => setHideNickAvatarEvents(!hideNickAvatarEvents)}
             />
-          )}
-          content={<Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>}
+          }
+          content={
+            <Text variant="b3">Hide nick and avatar change messages from room timeline.</Text>
+          }
         />
         <SettingTile
           title="Disable media auto load"
-          options={(
-            <Toggle
-              isActive={!mediaAutoLoad}
-              onToggle={() => setMediaAutoLoad(!mediaAutoLoad)}
-            />
-          )}
-          content={<Text variant="b3">Prevent images and videos from auto loading to save bandwidth.</Text>}
+          options={
+            <Toggle isActive={!mediaAutoLoad} onToggle={() => setMediaAutoLoad(!mediaAutoLoad)} />
+          }
+          content={
+            <Text variant="b3">Prevent images and videos from auto loading to save bandwidth.</Text>
+          }
         />
         <SettingTile
           title="Url Preview"
-          options={(
-            <Toggle
-              isActive={urlPreview}
-              onToggle={() => setUrlPreview(!urlPreview)}
-            />
-          )}
+          options={<Toggle isActive={urlPreview} onToggle={() => setUrlPreview(!urlPreview)} />}
           content={<Text variant="b3">Show url preview for link in messages.</Text>}
         />
         <SettingTile
           title="Url Preview in Encrypted Room"
-          options={(
-            <Toggle
-              isActive={encUrlPreview}
-              onToggle={() => setEncUrlPreview(!encUrlPreview)}
-            />
-          )}
+          options={
+            <Toggle isActive={encUrlPreview} onToggle={() => setEncUrlPreview(!encUrlPreview)} />
+          }
           content={<Text variant="b3">Show url preview for link in encrypted messages.</Text>}
         />
         <SettingTile
           title="Show hidden events"
-          options={(
+          options={
             <Toggle
               isActive={showHiddenEvents}
               onToggle={() => setShowHiddenEvents(!showHiddenEvents)}
             />
-          )}
+          }
           content={<Text variant="b3">Show hidden state and message events.</Text>}
         />
       </div>
@@ -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 <Text className="settings-notifications__not-supported">Not supported in this browser.</Text>;
+      return (
+        <Text className="settings-notifications__not-supported">
+          Not supported in this browser.
+        </Text>
+      );
     }
 
     if (notifPermission === 'denied') {
-      return <Text>Permission Denied</Text>
+      return <Text>Permission Denied</Text>;
     }
-    
+
     if (notifPermission === 'granted') {
       return (
         <Toggle
@@ -256,9 +309,11 @@ function NotificationsSection() {
     return (
       <Button
         variant="primary"
-        onClick={() => window.Notification.requestPermission().then(() => {
-          setShowNotifications(window.Notification?.permission === 'granted');
-        })}
+        onClick={() =>
+          window.Notification.requestPermission().then(() => {
+            setShowNotifications(window.Notification?.permission === 'granted');
+          })
+        }
       >
         Request permission
       </Button>
@@ -276,12 +331,12 @@ function NotificationsSection() {
         />
         <SettingTile
           title="Notification Sound"
-          options={(
+          options={
             <Toggle
               isActive={isNotificationSounds}
               onToggle={() => setIsNotificationSounds(!isNotificationSounds)}
             />
-            )}
+          }
           content={<Text variant="b3">Play sound when new messages arrive.</Text>}
         />
       </div>
@@ -295,8 +350,12 @@ function NotificationsSection() {
 function EmojiSection() {
   return (
     <>
-      <div className="settings-emoji__card"><ImagePackUser /></div>
-      <div className="settings-emoji__card"><ImagePackGlobal /></div>
+      <div className="settings-emoji__card">
+        <ImagePackUser />
+      </div>
+      <div className="settings-emoji__card">
+        <ImagePackGlobal />
+      </div>
     </>
   );
 }
@@ -314,21 +373,29 @@ function SecuritySection() {
         <MenuHeader>Export/Import encryption keys</MenuHeader>
         <SettingTile
           title="Export E2E room keys"
-          content={(
+          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>
+              <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={(
+          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>
+              <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>
@@ -337,7 +404,7 @@ function SecuritySection() {
 
 function AboutSection() {
   const mx = useMatrixClient();
-  
+
   return (
     <div className="settings-about">
       <div className="settings-about__card">
@@ -347,14 +414,21 @@ function AboutSection() {
           <div>
             <Text variant="h2" weight="medium">
               Cinny
-              <span className="text text-b3" style={{ margin: '0 var(--sp-extra-tight)' }}>{`v${cons.version}`}</span>
+              <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__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={() => clearCacheAndReload(mx)} variant="danger">Clear cache & reload</Button>
+              <Button onClick={() => clearCacheAndReload(mx)} variant="danger">
+                Clear cache & reload
+              </Button>
             </div>
           </div>
         </div>
@@ -364,20 +438,104 @@ function AboutSection() {
         <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>
+              {/* 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://github.com/mozilla/twemoji-colr" target="_blank" rel="noreferrer noopener">twemoji-colr</a> font is © <a href="https://mozilla.org/" target="_blank" rel="noreferrer noopener">Mozilla Foundation</a> used under the terms of <a href="http://www.apache.org/licenses/LICENSE-2.0" target="_blank" rel="noreferrer noopener">Apache 2.0</a>.</Text>
+              {/* eslint-disable-next-line react/jsx-one-expression-per-line */}
+              <Text>
+                The{' '}
+                <a
+                  href="https://github.com/mozilla/twemoji-colr"
+                  target="_blank"
+                  rel="noreferrer noopener"
+                >
+                  twemoji-colr
+                </a>{' '}
+                font is ©{' '}
+                <a href="https://mozilla.org/" target="_blank" rel="noreferrer noopener">
+                  Mozilla Foundation
+                </a>{' '}
+                used under the terms of{' '}
+                <a
+                  href="http://www.apache.org/licenses/LICENSE-2.0"
+                  target="_blank"
+                  rel="noreferrer noopener"
+                >
+                  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>
+              {/* 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>
+              {/* 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>
@@ -393,32 +551,38 @@ export const tabText = {
   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.EMOJI,
-  iconSrc: EmojiIC,
-  disabled: false,
-  render: () => <EmojiSection />,
-}, {
-  text: tabText.SECURITY,
-  iconSrc: LockIC,
-  disabled: false,
-  render: () => <SecuritySection />,
-}, {
-  text: tabText.ABOUT,
-  iconSrc: InfoIC,
-  disabled: false,
-  render: () => <AboutSection />,
-}];
+const tabItems = [
+  {
+    text: tabText.APPEARANCE,
+    iconSrc: SunIC,
+    disabled: false,
+    render: () => <AppearanceSection />,
+  },
+  {
+    text: tabText.NOTIFICATIONS,
+    iconSrc: BellIC,
+    disabled: false,
+    render: () => <NotificationsSection />,
+  },
+  {
+    text: tabText.EMOJI,
+    iconSrc: EmojiIC,
+    disabled: false,
+    render: () => <EmojiSection />,
+  },
+  {
+    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);
@@ -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() {
     <PopupWindow
       isOpen={isOpen}
       className="settings-window"
-      title={<Text variant="s1" weight="medium" primary>Settings</Text>}
-      contentOptions={(
+      title={
+        <Text variant="s1" weight="medium" primary>
+          Settings
+        </Text>
+      }
+      contentOptions={
         <>
           <Button variant="danger" iconSrc={PowerIC} onClick={handleLogout}>
             Logout
           </Button>
           <IconButton src={CrossIC} onClick={requestClose} tooltip="Close" />
         </>
-      )}
+      }
       onRequestClose={requestClose}
     >
       {isOpen && (
@@ -475,9 +650,7 @@ function Settings() {
             defaultSelected={tabItems.findIndex((tab) => tab.text === selectedTab.text)}
             onSelect={handleTabChange}
           />
-          <div className="settings-window__cards-wrapper">
-            { selectedTab.render() }
-          </div>
+          <div className="settings-window__cards-wrapper">{selectedTab.render()}</div>
         </div>
       )}
     </PopupWindow>
index 5f557aa72393566b56ae9d820c0287afcd003fbd..845fceb3ff1c7a913d40c826f28ac65aa20254b8 100644 (file)
@@ -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 (
     <>
+      <SystemEmojiFeature />
+      <PageZoomFeature />
       <FaviconUpdater />
       <InviteNotifications />
       <MessageNotifications />
index a590e0bc5a09badb2f641e413b424aa8b5cda3fa..f30bf63a59bc1be5ccf2e13d135c9d4bd65dfa2c 100644 (file)
@@ -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 (
     <SplashScreen>
@@ -198,7 +184,7 @@ export function ClientRoot({ children }: ClientRootProps) {
                 {startState.status === AsyncStatus.Error && (
                   <Text>{`Failed to load. ${startState.error.message}`}</Text>
                 )}
-                <Button variant="Critical" onClick={loadMatrix}>
+                <Button variant="Critical" onClick={mx ? () => startMatrix(mx) : loadMatrix}>
                   <Text as="span" size="B400">
                     Retry
                   </Text>
@@ -220,7 +206,6 @@ export function ClientRoot({ children }: ClientRootProps) {
                   <Windows />
                   <Dialogs />
                   <ReusableContextMenu />
-                  <SystemEmojiFeature />
                 </MediaConfigProvider>
               </CapabilitiesProvider>
             )}
index 061931ea82e20f53e2bdf266cda84705e45747fc..e438024958cebc910ee9c4900432ac884dc534e3 100644 (file)
@@ -10,6 +10,7 @@ export interface Settings {
   isMarkdown: boolean;
   editorToolbar: boolean;
   twitterEmoji: boolean;
+  pageZoom: number;
 
   isPeopleDrawer: boolean;
   memberSortFilterIndex: number;
@@ -33,6 +34,7 @@ const defaultSettings: Settings = {
   isMarkdown: true,
   editorToolbar: false,
   twitterEmoji: false,
+  pageZoom: 100,
 
   isPeopleDrawer: true,
   memberSortFilterIndex: 0,