feat(settings): split General into pages; collapse the big pickers (#459, #460)

#459 General held 76 of ~110 settings (11 screens). Its sections now live
on pages named for what people come to do:
  Account, Appearance, Messages (incl. editor, date and time),
  Notifications, Calls & audio, Privacy & security (privacy options +
  the former Devices page), Data & storage (storage, sync, your data),
  Desktop (desktop app only), Emojis & stickers, Advanced (the former
  Developer Tools + "show hidden events" / "legacy username colour"),
  About.
The longest page is now ~3 screens (Appearance). Settings opens on
Account. Deep links keep working: "devices" lands on Privacy & security,
the old "general" on Appearance, the update notice on Desktop. Text that
pointed at the old page names is updated. The push-rule editor gets one
line explaining the Matrix terms. Remaining Title Case labels on Account
and Devices are sentence case.

#460 Chat background, seasonal theme and avatar decoration grids sit
behind one row: the current choice and a Change button (the first two
close after a choice; decorations keep their Save step).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_017PCb59uY3UKqDqt7B6hzd5
This commit is contained in:
Lotus CI
2026-10-25 11:56:24 -04:00
committed by jared
co-authored by Claude Opus 5.5
parent 299da879fb
commit 449d0c740e
23 changed files with 392 additions and 200 deletions
+52 -43
View File
@@ -15,7 +15,16 @@ import {
Text,
} from 'folds';
import FocusTrap from 'focus-trap-react';
import { General } from './general';
import {
AdvancedDisplayOptions,
AppearancePage,
CallsPage,
DataStoragePage,
DesktopPage,
MessagesPage,
Privacy,
} from './general';
import { isTauri as isTauriEnv } from '../../hooks/useTauri';
import { PageNav, PageNavContent, PageNavHeader, PageRoot } from '../../components/page';
import { ScreenSize, useScreenSizeContext } from '../../hooks/useScreenSize';
import { Account } from './account';
@@ -45,43 +54,23 @@ type SettingsMenuItem = {
const useSettingsMenuItems = (): SettingsMenuItem[] =>
useMemo(
() => [
{
page: SettingsPages.GeneralPage,
name: 'General',
icon: Icons.Setting,
},
{
page: SettingsPages.AccountPage,
name: 'Account',
icon: Icons.User,
},
{
page: SettingsPages.NotificationPage,
name: 'Notifications',
icon: Icons.Bell,
},
{
page: SettingsPages.DevicesPage,
name: 'Devices',
icon: Icons.Monitor,
},
{
page: SettingsPages.EmojisStickersPage,
name: 'Emojis & Stickers',
icon: Icons.Smile,
},
{
page: SettingsPages.DeveloperToolsPage,
name: 'Developer Tools',
icon: Icons.Terminal,
},
{
page: SettingsPages.AboutPage,
name: 'About',
icon: Icons.Info,
},
],
() =>
[
{ page: SettingsPages.AccountPage, name: 'Account', icon: Icons.User },
{ page: SettingsPages.AppearancePage, name: 'Appearance', icon: Icons.Sun },
{ page: SettingsPages.MessagesPage, name: 'Messages', icon: Icons.Message },
{ page: SettingsPages.NotificationPage, name: 'Notifications', icon: Icons.Bell },
{ page: SettingsPages.CallsPage, name: 'Calls & audio', icon: Icons.Phone },
{ page: SettingsPages.DevicesPage, name: 'Privacy & security', icon: Icons.ShieldUser },
{ page: SettingsPages.DataStoragePage, name: 'Data & storage', icon: Icons.Download },
// [Gitea #459] Desktop-only options: nothing to show in a browser.
...(isTauriEnv()
? [{ page: SettingsPages.DesktopPage, name: 'Desktop', icon: Icons.Monitor }]
: []),
{ page: SettingsPages.EmojisStickersPage, name: 'Emojis & stickers', icon: Icons.Smile },
{ page: SettingsPages.DeveloperToolsPage, name: 'Advanced', icon: Icons.Terminal },
{ page: SettingsPages.AboutPage, name: 'About', icon: Icons.Info },
] as SettingsMenuItem[],
[],
);
@@ -102,7 +91,7 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
const screenSize = useScreenSizeContext();
const [activePage, setActivePage] = useState<SettingsPages | undefined>(() => {
if (initialPage) return initialPage;
return screenSize === ScreenSize.Mobile ? undefined : SettingsPages.GeneralPage;
return screenSize === ScreenSize.Mobile ? undefined : SettingsPages.AccountPage;
});
const menuItems = useSettingsMenuItems();
@@ -207,8 +196,20 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
)
}
>
{activePage === SettingsPages.GeneralPage && (
<General requestClose={handlePageRequestClose} />
{activePage === SettingsPages.AppearancePage && (
<AppearancePage requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.MessagesPage && (
<MessagesPage requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.CallsPage && (
<CallsPage requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.DataStoragePage && (
<DataStoragePage requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.DesktopPage && (
<DesktopPage requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.AccountPage && (
<Account requestClose={handlePageRequestClose} />
@@ -217,13 +218,21 @@ export function Settings({ initialPage, requestClose }: SettingsProps) {
<Notifications requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.DevicesPage && (
<Devices requestClose={handlePageRequestClose} />
<Devices
title="Privacy & security"
before={<Privacy />}
requestClose={handlePageRequestClose}
/>
)}
{activePage === SettingsPages.EmojisStickersPage && (
<EmojisStickers requestClose={handlePageRequestClose} />
)}
{activePage === SettingsPages.DeveloperToolsPage && (
<DeveloperTools requestClose={handlePageRequestClose} />
<DeveloperTools
title="Advanced"
before={<AdvancedDisplayOptions />}
requestClose={handlePageRequestClose}
/>
)}
{activePage === SettingsPages.AboutPage && <About requestClose={handlePageRequestClose} />}
</PageRoot>
@@ -313,7 +313,7 @@ function ProfileDisplayName({ profile, userId }: ProfileProps) {
<SettingTile
title={
<Text as="span" size="L400">
Display Name
Display name
</Text>
}
>
@@ -576,7 +576,7 @@ function ProfileStatus() {
<SettingTile
title={
<Text as="span" size="L400">
Status Message
Status message
</Text>
}
description={
@@ -102,6 +102,8 @@ export function ProfileDecoration() {
const [current, setCurrent] = useState<string | null>(null);
const [selected, setSelected] = useState<string | null>(null);
// [Gitea #460] The catalog stays closed until "Change"; it was always open.
const [browsing, setBrowsing] = useState(false);
// Distinguish "confirmed no decoration" from "failed to load": a fetch
// failure must not be shown as (and saved over) "None".
const [loadError, setLoadError] = useState(false);
@@ -199,7 +201,7 @@ export function ProfileDecoration() {
<SettingTile
title={
<Text as="span" size="L400">
Avatar Decoration
Avatar decoration
</Text>
}
description={
@@ -282,6 +284,20 @@ export function ProfileDecoration() {
</Button>
)}
</Box>
{!loadError && (
<Button
type="button"
size="300"
radii="300"
variant="Secondary"
fill="Soft"
outlined
aria-expanded={browsing}
onClick={() => setBrowsing((b) => !b)}
>
<Text size="B300">{browsing ? 'Done' : 'Change'}</Text>
</Button>
)}
{hasChanges && !loadError && (
<Button
type="button"
@@ -324,73 +340,77 @@ export function ProfileDecoration() {
</Text>
)}
{/* Search + category tabs. Only one category (or the search results)
{browsing && (
<>
{/* Search + category tabs. Only one category (or the search results)
is mounted at a time so the picker never loads the whole catalog. */}
<Input
aria-label="Search decorations"
placeholder="Search decorations"
value={search}
onChange={(e: ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
variant="Secondary"
size="400"
radii="300"
before={<Icon size="100" src={Icons.Search} />}
/>
{!query && (
<Box gap="100" wrap="Wrap" role="tablist" aria-label="Decoration categories">
{DECORATION_CATEGORIES.map((category) => (
<Chip
key={category.id}
type="button"
role="tab"
aria-selected={category.id === activeCategory}
variant={category.id === activeCategory ? 'Primary' : 'Secondary'}
fill="Soft"
outlined={category.id === activeCategory}
radii="Pill"
onClick={() => setActiveCategory(category.id)}
>
<Text as="span" size="B300">
{category.label}
</Text>
</Chip>
))}
</Box>
)}
<div
role={query ? undefined : 'tabpanel'}
style={{
maxHeight: 360,
overflowY: 'auto',
overflowX: 'hidden',
paddingRight: 4,
}}
>
{visible.length === 0 ? (
<Text size="T300" priority="300">
No decorations match “{search.trim()}”.
</Text>
) : (
<Input
aria-label="Search decorations"
placeholder="Search decorations"
value={search}
onChange={(e: ChangeEvent<HTMLInputElement>) => setSearch(e.target.value)}
variant="Secondary"
size="400"
radii="300"
before={<Icon size="100" src={Icons.Search} />}
/>
{!query && (
<Box gap="100" wrap="Wrap" role="tablist" aria-label="Decoration categories">
{DECORATION_CATEGORIES.map((category) => (
<Chip
key={category.id}
type="button"
role="tab"
aria-selected={category.id === activeCategory}
variant={category.id === activeCategory ? 'Primary' : 'Secondary'}
fill="Soft"
outlined={category.id === activeCategory}
radii="Pill"
onClick={() => setActiveCategory(category.id)}
>
<Text as="span" size="B300">
{category.label}
</Text>
</Chip>
))}
</Box>
)}
<div
role={query ? undefined : 'tabpanel'}
style={{
display: 'grid',
gridTemplateColumns: `repeat(auto-fill, ${CELL_SIZE}px)`,
gap: 20,
padding: 2,
maxHeight: 360,
overflowY: 'auto',
overflowX: 'hidden',
paddingRight: 4,
}}
>
{visible.map((d) => (
<DecorationPreviewCell
key={d.slug}
slug={d.slug}
name={d.name}
selected={selected === d.slug}
onSelect={handleSelect}
/>
))}
{visible.length === 0 ? (
<Text size="T300" priority="300">
No decorations match “{search.trim()}”.
</Text>
) : (
<div
style={{
display: 'grid',
gridTemplateColumns: `repeat(auto-fill, ${CELL_SIZE}px)`,
gap: 20,
padding: 2,
}}
>
{visible.map((d) => (
<DecorationPreviewCell
key={d.slug}
slug={d.slug}
name={d.name}
selected={selected === d.slug}
onSelect={handleSelect}
/>
))}
</div>
)}
</div>
)}
</div>
</>
)}
</Box>
</SettingTile>
);
@@ -1,4 +1,4 @@
import React, { useCallback, useState } from 'react';
import React, { useCallback, useState, ReactNode } from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll, Switch, Button } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
@@ -18,8 +18,12 @@ import { CryptoDiagnostics } from '../developer/CryptoDiagnostics';
type DeveloperToolsProps = {
requestClose: () => void;
/** [Gitea #459] Shown as the page title (the menu entry's name). */
title?: string;
/** [Gitea #459] Sections shown above this page's own. */
before?: ReactNode;
};
export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
export function DeveloperTools({ requestClose, title, before }: DeveloperToolsProps) {
const mx = useMatrixClient();
const [developerTools, setDeveloperTools] = useSetting(settingsAtom, 'developerTools');
const [expand, setExpend] = useState(false);
@@ -49,7 +53,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Developer Tools
{title ?? 'Developer Tools'}
</Text>
</Box>
<Box shrink="No">
@@ -63,6 +67,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
{before}
<Box direction="Column" gap="100">
<Text size="L400">Options</Text>
<SequenceCard
@@ -137,7 +137,7 @@ function DeviceRename({ device, onCancel, onRename, refreshDeviceList }: DeviceR
return (
<Box as="form" onSubmit={handleSubmit} direction="Column" gap="100">
<Text size="L400">Device Name</Text>
<Text size="L400">Device name</Text>
<Box gap="200">
<Box grow="Yes" direction="Column">
<Input
@@ -1,4 +1,4 @@
import React from 'react';
import React, { ReactNode } from 'react';
import { Box, Text, IconButton, Icon, Icons, Scroll } from 'folds';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
@@ -38,8 +38,12 @@ function DevicesPlaceholder() {
type DevicesProps = {
requestClose: () => void;
/** [Gitea #459] Shown as the page title (the menu entry's name). */
title?: string;
/** [Gitea #459] Sections shown above this page's own. */
before?: ReactNode;
};
export function Devices({ requestClose }: DevicesProps) {
export function Devices({ requestClose, title, before }: DevicesProps) {
const mx = useMatrixClient();
const crypto = mx.getCrypto();
const crossSigningActive = useCrossSigningActive();
@@ -70,7 +74,7 @@ export function Devices({ requestClose }: DevicesProps) {
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
Devices
{title ?? 'Devices'}
</Text>
</Box>
<Box shrink="No">
@@ -84,6 +88,7 @@ export function Devices({ requestClose }: DevicesProps) {
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
{before}
<Box direction="Column" gap="100">
<Text size="L400">Security</Text>
<SequenceCard
@@ -67,7 +67,7 @@ function ExportKeys() {
{(match, doMatch, passRef, confPassRef) => (
<>
<Box grow="Yes" direction="Column" gap="100">
<Text size="L400">New Password</Text>
<Text size="L400">New password</Text>
<PasswordInput
ref={passRef}
name="passwordInput"
@@ -82,7 +82,7 @@ function ExportKeys() {
/>
</Box>
<Box grow="Yes" direction="Column" gap="100">
<Text size="L400">Confirm Password</Text>
<Text size="L400">Confirm password</Text>
<PasswordInput
ref={confPassRef}
style={{ color: match ? undefined : color.Critical.Main }}
+177 -57
View File
@@ -4,6 +4,7 @@ import React, {
FormEventHandler,
KeyboardEventHandler,
MouseEventHandler,
ReactNode,
useCallback,
useEffect,
useMemo,
@@ -543,7 +544,7 @@ function PageZoomInput() {
);
}
function Appearance() {
export function Appearance() {
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const allThemes = useThemes();
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
@@ -595,7 +596,7 @@ function Appearance() {
settingsAtom,
'seasonalThemeOverride',
);
const [, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground');
return (
<Box direction="Column" gap="100">
@@ -672,13 +673,13 @@ function Appearance() {
direction="Column"
gap="200"
>
<SettingTile
<CollapsiblePicker
title="Chat background"
description="Pattern applied behind the message timeline."
/>
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
current={BG_OPTIONS.find((o) => o.value === chatBackground)?.label ?? 'None'}
>
<ChatBgGrid />
</Box>
</CollapsiblePicker>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
@@ -695,11 +696,14 @@ function Appearance() {
direction="Column"
gap="200"
>
<SettingTile
<CollapsiblePicker
title="Seasonal theme"
description="Decorative overlays for holidays and events. “Auto” follows the calendar — each theme below shows the dates it turns on. Click to select."
/>
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
description="Decorative overlays for holidays and events. Auto follows the calendar."
current={
SEASONAL_OPTIONS.find((o) => o.value === (seasonalThemeOverride ?? 'auto'))?.label ??
'Auto'
}
>
<SeasonalBgGrid
value={seasonalThemeOverride ?? 'auto'}
onChange={(v) => {
@@ -711,7 +715,7 @@ function Appearance() {
if (v !== 'off') setChatBackground('none');
}}
/>
</Box>
</CollapsiblePicker>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
@@ -1308,7 +1312,7 @@ function SelectDateFormat() {
);
}
function DateAndTime() {
export function DateAndTime() {
const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock');
return (
@@ -1487,7 +1491,7 @@ function ComposerToolbarReorder({
);
}
function Editor() {
export function Editor() {
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [offerCodePaste, setOfferCodePaste] = useSetting(settingsAtom, 'offerCodePaste');
@@ -1578,7 +1582,7 @@ function Editor() {
);
}
function Privacy() {
export function Privacy() {
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
const [hidePresence, setHidePresence] = useSetting(settingsAtom, 'hidePresence');
const [privateReadReceipts, setPrivateReadReceipts] = useSetting(
@@ -1655,7 +1659,7 @@ function Privacy() {
// [Gitea #104] Sync toggle + the two manual actions. The toggle is itself
// device-local (never synced) so switching it off here sticks.
function SettingsSyncSection() {
export function SettingsSyncSection() {
const isMobile = useScreenSize() === ScreenSize.Mobile;
const [settingsSync, setSettingsSync] = useSetting(settingsAtom, 'settingsSync');
const { pushNow, clearRemote } = useSettingsSyncActions();
@@ -1788,7 +1792,7 @@ function useKeyBind(setter: (code: string) => void, otherKey?: string) {
const keyLabel = (code: string) =>
code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', '');
function Calls() {
export function Calls() {
const [cameraOnJoin, setCameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin');
const [callNoiseSuppression, setCallNoiseSuppression] = useSetting(
settingsAtom,
@@ -2404,6 +2408,65 @@ const SEASONAL_OPTIONS: {
{ value: 'deepspace', label: 'Deep Space', emoji: '🚀' },
];
/**
* [Gitea #460] A picker grid behind one row: the current choice and a Change
* button. The big grids (27 backgrounds, 13 seasonal themes) used to be always
* open and made Appearance about 2.5 screens long. Closes after a choice.
*/
export function CollapsiblePicker({
title,
description,
current,
children,
}: {
title: string;
description?: ReactNode;
current: string;
children: ReactNode;
}) {
const [open, setOpen] = useState(false);
const [openedWith, setOpenedWith] = useState(current);
useEffect(() => {
if (open && current !== openedWith) setOpen(false);
}, [open, current, openedWith]);
const id = `picker-${title.replace(/[^a-z0-9]+/gi, '-').toLowerCase()}`;
return (
<>
<SettingTile
title={title}
description={description}
after={
<Box alignItems="Center" gap="200" shrink="No">
<Text size="T300" priority="300">
{current}
</Text>
<Button
size="300"
variant="Secondary"
fill="Soft"
radii="300"
outlined
aria-expanded={open}
aria-controls={id}
onClick={() => {
setOpenedWith(current);
setOpen((o) => !o);
}}
>
<Text size="B300">{open ? 'Done' : 'Change'}</Text>
</Button>
</Box>
}
/>
{open && (
<Box id={id} style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
{children}
</Box>
)}
</>
);
}
function SeasonalBgGrid({
value,
onChange,
@@ -2663,11 +2726,7 @@ function SelectMessageSpacing() {
);
}
function Messages() {
const [legacyUsernameColor, setLegacyUsernameColor] = useSetting(
settingsAtom,
'legacyUsernameColor',
);
export function Messages() {
const [hideMembershipEvents, setHideMembershipEvents] = useSetting(
settingsAtom,
'hideMembershipEvents',
@@ -2680,7 +2739,6 @@ function Messages() {
const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview');
const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview');
const [inlineMediaEmbeds, setInlineMediaEmbeds] = useSetting(settingsAtom, 'inlineMediaEmbeds');
const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
const [enforceRetentionLocally, setEnforceRetentionLocally] = useSetting(
settingsAtom,
'enforceRetentionLocally',
@@ -2707,18 +2765,6 @@ function Messages() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Message spacing" after={<SelectMessageSpacing />} />
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Legacy username colour"
after={
<Switch
variant="Primary"
value={legacyUsernameColor}
onChange={setLegacyUsernameColor}
/>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide joins and leaves"
@@ -2808,14 +2854,6 @@ function Messages() {
/>
</SequenceCard>
)}
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Show hidden events"
after={
<Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Enforce message retention"
@@ -2940,7 +2978,7 @@ function PackageUpdate({ help }: { help: NonNullable<ReturnType<typeof packageUp
* inside Appearance, between Monochrome Mode and Twitter Emoji). Renders
* nothing outside the desktop app.
*/
function DesktopSettings() {
export function DesktopSettings() {
if (!isTauriEnv()) return null;
return (
<Box direction="Column" gap="100">
@@ -2952,7 +2990,7 @@ function DesktopSettings() {
);
}
function AppUpdates() {
export function AppUpdates() {
const { isTauri, status, check, install, installKind } = useTauriUpdater();
if (!isTauri) return null;
const packageHelp = packageUpdateHelp(installKind);
@@ -3031,14 +3069,60 @@ function AppUpdates() {
type GeneralProps = {
requestClose: () => void;
};
export function General({ requestClose }: GeneralProps) {
/** [Gitea #459] Display options for people who want to see the plumbing. */
export function AdvancedDisplayOptions() {
const [legacyUsernameColor, setLegacyUsernameColor] = useSetting(
settingsAtom,
'legacyUsernameColor',
);
const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents');
return (
<Box direction="Column" gap="100">
<Text size="L400">Display</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Legacy username colour"
after={
<Switch
variant="Primary"
value={legacyUsernameColor}
onChange={setLegacyUsernameColor}
/>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Show hidden events"
after={
<Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
}
/>
</SequenceCard>
</Box>
);
}
/**
* [Gitea #459] General used to hold 76 of the ~110 settings (11 screens). Its
* sections now live on pages named for what people come to do; Privacy goes
* to Privacy & security (devices/Devices.tsx) and the display plumbing to
* Advanced (developer-tools).
*/
function SectionPage({
title,
requestClose,
children,
}: GeneralProps & { title: string; children: ReactNode }) {
return (
<Page>
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
General
{title}
</Text>
</Box>
<Box shrink="No">
@@ -3052,17 +3136,7 @@ export function General({ requestClose }: GeneralProps) {
<Scroll hideTrack visibility="Hover">
<PageContent>
<Box direction="Column" gap="700">
<Appearance />
<DateAndTime />
<Editor />
<Messages />
<Privacy />
<StorageUsage />
<SettingsSyncSection />
<YourDataSection />
<Calls />
<DesktopSettings />
<AppUpdates />
{children}
</Box>
</PageContent>
</Scroll>
@@ -3070,3 +3144,49 @@ export function General({ requestClose }: GeneralProps) {
</Page>
);
}
export function AppearancePage({ requestClose }: GeneralProps) {
return (
<SectionPage title="Appearance" requestClose={requestClose}>
<Appearance />
</SectionPage>
);
}
export function MessagesPage({ requestClose }: GeneralProps) {
return (
<SectionPage title="Messages" requestClose={requestClose}>
<Messages />
<Editor />
<DateAndTime />
</SectionPage>
);
}
export function CallsPage({ requestClose }: GeneralProps) {
return (
<SectionPage title="Calls & audio" requestClose={requestClose}>
<Calls />
</SectionPage>
);
}
export function DataStoragePage({ requestClose }: GeneralProps) {
return (
<SectionPage title="Data & storage" requestClose={requestClose}>
<StorageUsage />
<SettingsSyncSection />
<YourDataSection />
</SectionPage>
);
}
/** Desktop app only (the menu hides it elsewhere). */
export function DesktopPage({ requestClose }: GeneralProps) {
return (
<SectionPage title="Desktop" requestClose={requestClose}>
<DesktopSettings />
<AppUpdates />
</SectionPage>
);
}
@@ -334,6 +334,11 @@ export function PushRuleEditor() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Advanced push rules</Text>
{/* [Gitea #459] Matrix terms kept, with one line on what they are. */}
<Text size="T200" priority="300">
The raw Matrix notification rules behind the options above, checked in this order: override,
room, sender, then underride. You rarely need to change these.
</Text>
<RuleSection title="Override rules" kind={PushRuleKind.Override} rules={overrideRules} />
<RuleSection
title="Room rules"
+9 -1
View File
@@ -1,11 +1,19 @@
/** Settings dialog pages. Kept apart from Settings.tsx so callers can name a
* page without pulling the dialog into the entry chunk. */
export enum SettingsPages {
GeneralPage,
AccountPage,
// [Gitea #459] These replace the old catch-all General page.
AppearancePage,
MessagesPage,
NotificationPage,
CallsPage,
/** "Privacy & security": privacy options plus devices and encryption. */
DevicesPage,
DataStoragePage,
/** Desktop app only. */
DesktopPage,
EmojisStickersPage,
/** "Advanced": developer tools and display plumbing. */
DeveloperToolsPage,
AboutPage,
}