#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:
co-authored by
Claude Opus 5.5
parent
299da879fb
commit
449d0c740e
@@ -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 }}
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user