feat(settings): quick wins from the settings audit (#453)
CI / Build & Quality Checks (push) Skipped
CI / Secret scan (gitleaks) (push) Skipped
CI / Playwright smoke (e2e) (push) Skipped
CI / Build & Quality Checks (pull_request) Successful in 3m18s
CI / Trigger Desktop Build (pull_request) Skipped
CI / Docker image build & smoke test (pull_request) Skipped
CI / Secret scan (gitleaks) (pull_request) Successful in 12s
CI / Playwright smoke (e2e) (pull_request) Successful in 24m56s

- #456 Copy pass: sentence case for every setting title and section,
  plainer names ("Hide joins and leaves", "Link previews", "Emoji
  style: Twemoji", "Rejoin calls after a restart", "Also use the
  browser's noise suppression", "Push-to-talk key"...) and grammar fixes
  ("when messages arrive", "Play a sound when a new message arrives",
  blocked users "Enter their Matrix ID"). Labels only: no setting is
  renamed in storage.
- #455 One theme control: Theme: System / Light / Dark. System shows the
  light and dark variant pickers, Light or Dark one picker of that kind.
  Replaces the System switch plus a Theme dropdown that was greyed out
  whenever the switch was on. Uses the existing settings (no migration);
  picking a variant also remembers it for System.
- #457 Remove the "Block Messages" stub from Notifications; the GIF
  picker row only appears in builds with a GIF key.
- #458 (part 1) "Export/Import Messages Data" -> "Export/Import
  encryption keys", with a sentence on when you need it.

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 01:37:37 -04:00
co-authored by Claude Opus 5.5
parent 0d4f2e4a87
commit 5d15013a27
21 changed files with 199 additions and 160 deletions
+1 -1
View File
@@ -205,7 +205,7 @@ export function About({ requestClose }: AboutProps) {
gap="400"
>
<SettingTile
title="Clear Cache & Reload"
title="Clear cache and reload"
description="Clear all your locally stored data and reload from server. This also deletes this device's encryption keys — older encrypted messages stay unreadable here unless key backup is set up."
after={
<Button
@@ -22,14 +22,14 @@ export function ContactInformation() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Contact Information</Text>
<Text size="L400">Contact information</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile title="Email Address" description="Email address attached to your account.">
<SettingTile title="Email address" description="Email address attached to your account.">
<Box>
{emailIds?.map((email) => (
<Chip key={email.address} as="span" variant="Secondary" radii="Pill">
@@ -136,7 +136,7 @@ export function IgnoredUserList() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">Blocked Users</Text>
<Text size="L400">Blocked users</Text>
</Box>
<SequenceCard
className={SequenceCardStyle}
@@ -145,8 +145,8 @@ export function IgnoredUserList() {
gap="400"
>
<SettingTile
title="Select User"
description="Prevent receiving messages or invites from user by adding their userId."
title="Block someone"
description="Stop someone's messages and invites reaching you. Enter their Matrix ID, e.g. @name:server."
>
<Box direction="Column" gap="300">
<IgnoreUserInput userList={ignoredUsers} />
@@ -31,7 +31,7 @@ export function OidcManageAccount() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Account Management</Text>
<Text size="L400">Account management</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@@ -72,7 +72,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
gap="400"
>
<SettingTile
title="Enable Developer Tools"
title="Enable developer tools"
after={
<Switch
variant="Primary"
@@ -90,7 +90,7 @@ export function DeveloperTools({ requestClose }: DeveloperToolsProps) {
gap="400"
>
<SettingTile
title="Access Token"
title="Access token"
description="Copy access token to clipboard."
after={
<Button
@@ -35,7 +35,7 @@ export function CryptoDiagnostics() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Crypto Diagnostics</Text>
<Text size="L400">Crypto diagnostics</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@@ -43,8 +43,8 @@ export function CryptoDiagnostics() {
gap="400"
>
<SettingTile
title="Crypto Diagnostics — captures E2EE error signatures this session"
description="Ring-buffers up to 200 matched console warnings/errors for the KE-1..KE-4 bug cluster. Local only — no network calls. The downloaded report includes the matched log lines as evidence."
title="Crypto diagnostics"
description="Captures encryption error signatures from this session. Ring-buffers up to 200 matched console warnings/errors for the KE-1..KE-4 bug cluster. Local only — no network calls. The downloaded report includes the matched log lines as evidence."
after={
<Box alignItems="Center" gap="200" shrink="No">
<Badge variant={count > 0 ? 'Critical' : 'Secondary'} fill="Solid" radii="Pill">
@@ -93,8 +93,8 @@ export function Devices({ requestClose }: DevicesProps) {
gap="400"
>
<SettingTile
title="Device Verification"
description="To verify device identity and grant access to encrypted messages."
title="Device verification"
description="Verify this device so it can read your encrypted messages and others can trust it."
after={
<>
<EnableVerification visible={!crossSigningActive} />
@@ -130,8 +130,8 @@ function ExportKeysTile() {
return (
<>
<SettingTile
title="Export Messages Data"
description="Save password protected copy of encryption data on your device to decrypt messages later."
title="Export encryption keys"
description="Save a password-protected file of your encryption keys. Useful when moving to a device without key backup, or as an offline copy."
after={
<Box>
<Button
@@ -260,8 +260,8 @@ function ImportKeysTile() {
return (
<>
<SettingTile
title="Import Messages Data"
description="Load password protected copy of encryption data from device to decrypt your messages."
title="Import encryption keys"
description="Load a password-protected encryption key file to read older encrypted messages on this device."
after={
<Box>
{file ? (
@@ -307,7 +307,7 @@ function ImportKeysTile() {
export function LocalBackup() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Local Backup</Text>
<Text size="L400">Encryption key backup file</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@@ -135,7 +135,7 @@ export function OtherDevices({ devices, refreshDeviceList, showVerification }: O
gap="400"
>
<SettingTile
title="Device Dashboard"
title="Device dashboard"
description="Manage your devices on OIDC dashboard."
after={
<Button
@@ -423,7 +423,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
return (
<>
<Box direction="Column" gap="100">
<Text size="L400">Favorite Packs</Text>
<Text size="L400">Favourite packs</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
@@ -431,7 +431,7 @@ export function GlobalPacks({ onViewPack }: GlobalPacksProps) {
gap="400"
>
<SettingTile
title="Select Pack"
title="Add a pack"
description="Pick emojis and stickers pack from rooms to use in all rooms."
after={
<>
@@ -31,7 +31,7 @@ export function UserPack({ onViewPack }: UserPackProps) {
return (
<Box direction="Column" gap="100">
<Text size="L400">Default Pack</Text>
<Text size="L400">Default pack</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
+152 -101
View File
@@ -155,7 +155,7 @@ function DesktopChromeSetting() {
return (
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Custom Window Chrome (Beta)"
title="Custom window chrome (beta)"
description="Replace the system title bar with a Lotus-styled one. Desktop only — reloads to apply."
after={<Switch variant="Primary" value={customChrome} onChange={handleToggle} />}
/>
@@ -263,7 +263,7 @@ function KeychainMirrorSetting() {
}
return (
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Login in the system keychain" description={description} />
<SettingTile title="Keep your sign-in in the system keychain" description={description} />
</SequenceCard>
);
}
@@ -294,12 +294,18 @@ const ThemeSelector = as<'div', ThemeSelectorProps>(
),
);
function SelectTheme({ disabled }: { disabled?: boolean }) {
const themes = useThemes();
function SelectTheme({ disabled, kind }: { disabled?: boolean; kind?: ThemeKind }) {
const allThemes = useThemes();
const themes = kind ? allThemes.filter((theme) => theme.kind === kind) : allThemes;
const themeNames = useThemeNames();
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
const [, setLightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [, setDarkThemeId] = useSetting(settingsAtom, 'darkThemeId');
const [menuCords, setMenuCords] = useState<RectCords>();
const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme;
const selectedTheme =
themes.find((theme) => theme.id === themeId) ??
themes[0] ??
(kind === ThemeKind.Dark ? DarkTheme : LightTheme);
const handleThemeMenu: MouseEventHandler<HTMLButtonElement> = (evt) => {
setMenuCords(evt.currentTarget.getBoundingClientRect());
@@ -307,6 +313,10 @@ function SelectTheme({ disabled }: { disabled?: boolean }) {
const handleThemeSelect = (theme: Theme) => {
setThemeId(theme.id);
// [Gitea #455] Also remember it as this kind's variant, so switching to
// System later keeps the user's pick.
if (theme.kind === ThemeKind.Light) setLightThemeId(theme.id);
else setDarkThemeId(theme.id);
setMenuCords(undefined);
};
@@ -391,7 +401,7 @@ function SystemThemePreferences() {
return (
<Box wrap="Wrap" gap="400">
<SettingTile
title="Light Theme:"
title="Light theme:"
after={
<Chip
variant={themeKind === ThemeKind.Light ? 'Primary' : 'Secondary'}
@@ -432,7 +442,7 @@ function SystemThemePreferences() {
}
/>
<SettingTile
title="Dark Theme:"
title="Dark theme:"
after={
<Chip
variant={themeKind === ThemeKind.Dark ? 'Primary' : 'Secondary'}
@@ -534,6 +544,26 @@ function PageZoomInput() {
function Appearance() {
const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme');
const allThemes = useThemes();
const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId');
const [lightThemeId] = useSetting(settingsAtom, 'lightThemeId');
const [darkThemeId] = useSetting(settingsAtom, 'darkThemeId');
const currentKind = allThemes.find((t) => t.id === themeId)?.kind ?? ThemeKind.Light;
let themeMode: 'system' | 'light' | 'dark' = 'system';
if (!systemTheme) themeMode = currentKind === ThemeKind.Dark ? 'dark' : 'light';
const chooseThemeMode = (mode: 'system' | 'light' | 'dark') => {
if (mode === 'system') {
setSystemTheme(true);
return;
}
setSystemTheme(false);
// Use the variant already chosen for that kind (or the default one).
const kind = mode === 'dark' ? ThemeKind.Dark : ThemeKind.Light;
if (allThemes.find((t) => t.id === themeId)?.kind === kind) return;
const preferred = mode === 'dark' ? darkThemeId : lightThemeId;
const fallback = mode === 'dark' ? DarkTheme.id : LightTheme.id;
setThemeId(allThemes.find((t) => t.id === preferred && t.kind === kind)?.id ?? fallback);
};
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
const [perMessageProfiles, setPerMessageProfiles] = useSetting(
@@ -575,38 +605,64 @@ function Appearance() {
direction="Column"
gap="400"
>
<SettingTile
title="System Theme"
description="Choose between light and dark theme based on system preference."
after={<Switch variant="Primary" value={systemTheme} onChange={setSystemTheme} />}
/>
{systemTheme && <SystemThemePreferences />}
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
{/* [Gitea #455] One control instead of a System switch plus a Theme
dropdown that was greyed out whenever the switch was on. */}
<SettingTile
title="Theme"
description="Theme to use when system theme is not enabled."
after={<SelectTheme disabled={systemTheme} />}
description={
themeMode === 'system'
? "Follows your device's light or dark setting."
: 'Always use this theme, whatever your device is set to.'
}
after={
<Box gap="100" role="radiogroup" aria-label="Theme" shrink="No" wrap="Wrap">
{(
[
['system', 'System'],
['light', 'Light'],
['dark', 'Dark'],
] as const
).map(([mode, label]) => (
<Chip
key={mode}
role="radio"
aria-checked={themeMode === mode}
variant={themeMode === mode ? 'Primary' : 'Secondary'}
outlined={themeMode === mode}
radii="Pill"
onClick={() => chooseThemeMode(mode)}
>
<Text size="B300">{label}</Text>
</Chip>
))}
</Box>
}
/>
{themeMode === 'system' && <SystemThemePreferences />}
{themeMode !== 'system' && (
<SettingTile
title={themeMode === 'light' ? 'Light theme:' : 'Dark theme:'}
after={<SelectTheme kind={themeMode === 'light' ? ThemeKind.Light : ThemeKind.Dark} />}
/>
)}
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Monochrome Mode"
title="Monochrome mode"
after={<Switch variant="Primary" value={monochromeMode} onChange={setMonochromeMode} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Twitter Emoji"
title="Emoji style: Twemoji"
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Page Zoom" after={<PageZoomInput />} />
<SettingTile title="Page zoom" after={<PageZoomInput />} />
</SequenceCard>
<SequenceCard
@@ -616,7 +672,7 @@ function Appearance() {
gap="200"
>
<SettingTile
title="Chat Background"
title="Chat background"
description="Pattern applied behind the message timeline."
/>
<Box style={{ padding: `0 ${config.space.S400} ${config.space.S300}` }}>
@@ -626,7 +682,7 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Pause Background Animations"
title="Pause background animations"
description="Stop background animation to reduce motion or improve performance."
after={<Switch variant="Primary" value={pauseAnimations} onChange={setPauseAnimations} />}
/>
@@ -639,7 +695,7 @@ function Appearance() {
gap="200"
>
<SettingTile
title="Seasonal Theme"
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}` }}>
@@ -659,7 +715,7 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Show Profile on Every Message"
title="Show name and avatar on every message"
description="Display avatar and name on each message instead of grouping consecutive messages."
after={
<Switch variant="Primary" value={perMessageProfiles} onChange={setPerMessageProfiles} />
@@ -673,7 +729,7 @@ function Appearance() {
gap="200"
>
<SettingTile
title="Night Light"
title="Night light"
description="Reduce blue light with a warm orange tint overlay."
after={
<Switch variant="Primary" value={nightLightEnabled} onChange={setNightLightEnabled} />
@@ -753,7 +809,7 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Glassmorphism Sidebar"
title="Glass sidebar"
description="Semi-transparent sidebar with frosted glass effect. Works best with custom chat backgrounds."
after={
<Switch
@@ -767,8 +823,8 @@ function Appearance() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Lotus Terminal Mode"
description="LotusGuild Terminal Design System: Anduril Orange + Ice Cyan + Matrix Green, dot-grid background, CRT scanlines, and boot sequence animation (press Escape to skip)."
title="Lotus Terminal mode"
description="A retro terminal look: orange and cyan on a dot grid, CRT scanlines and a boot animation (press Esc to skip)."
after={
<Box direction="Row" gap="200" alignItems="Center">
{lotusTerminal && (
@@ -802,7 +858,7 @@ function Appearance() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="UI Font"
title="Interface font"
description="Font used throughout the interface."
after={
<SettingsSelect
@@ -822,7 +878,7 @@ function Appearance() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="@Mention Highlight Color"
title="@mention highlight colour"
description="Color used to highlight messages that mention you. Leave empty to use the theme default."
after={
<HexColorPickerPopOut
@@ -866,7 +922,7 @@ function Appearance() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Custom Accent Color"
title="Custom accent colour"
description={
lotusTerminal
? 'Only applies to non-TDS themes. Disable Lotus Terminal Mode to use a custom accent.'
@@ -1240,7 +1296,7 @@ function SelectDateFormat() {
return (
<>
<SettingTile
title="Date Format"
title="Date format"
description={customDateFormat ? dayjs().format(dateFormatString) : ''}
after={<PresetDateFormat value={selectedDateFormat} onChange={handlePresetChange} />}
/>
@@ -1256,10 +1312,10 @@ function DateAndTime() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Date & Time</Text>
<Text size="L400">Date and time</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="24-Hour Time Format"
title="24-hour time"
after={<Switch variant="Primary" value={hour24Clock} onChange={setHour24Clock} />}
/>
</SequenceCard>
@@ -1471,7 +1527,7 @@ function Editor() {
<Text size="L400">Editor</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="ENTER for Newline"
title="Enter for a new line"
description={`Use ${
isMacOS() ? KeySymbol.Command : 'Ctrl'
} + ENTER to send message and ENTER for newline.`}
@@ -1480,20 +1536,20 @@ function Editor() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Markdown Formatting"
title="Markdown formatting"
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Offer to Format Pasted Code"
title="Offer to format pasted code"
description="When something you paste looks like code, offer to turn it into a code block. Nothing changes unless you accept."
after={<Switch variant="Primary" value={offerCodePaste} onChange={setOfferCodePaste} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Formatting Toolbar"
title="Formatting toolbar"
description="Show bold, italic, code and other formatting buttons above the message input."
after={<Switch variant="Primary" value={editorToolbar} onChange={setEditorToolbar} />}
/>
@@ -1505,7 +1561,7 @@ function Editor() {
gap="300"
>
<SettingTile
title="Composer Toolbar"
title="Composer toolbar"
description="Drag to reorder buttons, and tap a button to show or hide it in the message composer."
/>
<Box direction="Column" style={{ paddingBottom: config.space.S200 }}>
@@ -1540,28 +1596,28 @@ function Privacy() {
<Text size="L400">Privacy</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Strip Tracking Parameters from Links"
title="Strip tracking parameters from links"
description="Remove utm_, fbclid, YouTube si= and other ad/analytics identifiers from links you paste or send, and from links shown in chat. Runs entirely on this device."
after={<Switch variant="Primary" value={stripTracking} onChange={setStripTracking} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Remove Photo Metadata Before Sending"
title="Remove photo metadata before sending"
description="Strip location, camera model and timestamp (EXIF/XMP) from JPEG, PNG and WebP images you send or set as an avatar. Pixels are untouched; the rotation is kept. Videos and GIFs are not covered."
after={<Switch variant="Primary" value={stripImageMeta} onChange={setStripImageMeta} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide Typing & Read Receipts"
title="Hide typing and read receipts"
description="Turn off both typing status and read receipts to keep your activity private."
after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Private Read Receipts"
title="Private read receipts"
description="Send read receipts only to you and the server — other users won't see when you've read messages."
after={
<Switch
@@ -1574,7 +1630,7 @@ function Privacy() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide Online Status"
title="Hide online status"
description="Appear offline to everyone while still using the app. Disables online, away, and idle indicators."
after={<Switch variant="Primary" value={hidePresence} onChange={setHidePresence} />}
/>
@@ -1656,7 +1712,7 @@ function SettingsSyncSection() {
<Text size="L400">Sync</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Sync Settings Across Devices"
title="Sync settings across devices"
description="Keep your preferences (theme, composer toolbar, notifications, quiet hours, call keys…) the same on every device, stored as account data on your own homeserver. Device-specific settings such as zoom, media auto-load, noise suppression and volumes stay local."
after={<Switch variant="Primary" value={settingsSync} onChange={setSettingsSync} />}
/>
@@ -1664,7 +1720,7 @@ function SettingsSyncSection() {
{settingsSync && (
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Use This Device's Settings Everywhere"
title="Use this device's settings everywhere"
description="Push this device's current preferences so every other device adopts them, even ones that changed something more recently."
// [Gitea #273] On phones the two buttons squeezed the text into a
// narrow column; put them under the description there.
@@ -1799,14 +1855,14 @@ function Calls() {
<Text size="L400">Calls</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Join with Camera On"
title="Join with camera on"
description="Enable camera automatically when joining a voice or video call. Camera is off by default."
after={<Switch variant="Primary" value={cameraOnJoin} onChange={setCameraOnJoin} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Noise Suppression"
title="Noise suppression"
description={
<Box direction="Column" gap="200">
{/* [Gitea #271] T200 like every other setting description. */}
@@ -1863,7 +1919,7 @@ function Calls() {
<Box direction="Column" gap="200">
<Text size="L400">Model</Text>
<SettingTile
title="ML Model"
title="ML model"
description="Choose the machine learning model used for noise removal. Heavier models clean more aggressively at a higher CPU cost."
after={
<SettingsSelect<DenoiseModelId>
@@ -1979,7 +2035,7 @@ function Calls() {
>
<Text size="L400">Enhancements</Text>
<SettingTile
title="Series Suppression"
title="Also use the browser's noise suppression"
description="Run the browser's native stationary noise filter before the ML model. Recommended for eliminating fan hum."
after={
<Switch
@@ -1991,7 +2047,7 @@ function Calls() {
/>
<SettingTile
title="Noise Gate"
title="Noise gate"
description="Hard-cut audio when you aren't speaking to ensure absolute silence between sentences."
after={
<Switch variant="Primary" value={callDenoiseGate} onChange={setCallDenoiseGate} />
@@ -2027,7 +2083,7 @@ function Calls() {
borderTop: `${config.borderWidth.B300} solid ${color.Surface.ContainerLine}`,
}}
>
<Text size="L400">Test &amp; calibrate</Text>
<Text size="L400">Test and calibrate</Text>
<Text size="T200" priority="300">
Audition the selected model and tune the gate without joining a call. Changes to the
settings above apply the next time you start a monitor or play a clip.
@@ -2049,7 +2105,7 @@ function Calls() {
gap="400"
>
<SettingTile
title="After a Restart"
title="Rejoin calls after a restart"
description="If Lotus crashes, reloads or restarts for an update while you are in a voice room, put you back in."
after={
<SettingsSelect<'ask' | 'auto' | 'off'>
@@ -2064,7 +2120,7 @@ function Calls() {
}
/>
<SettingTile
title="Summary After Calls"
title="Summary after calls"
description="When a call ends, show how long it was and how the connection held up, with a Details button for who was there, network numbers and your own talk/mute time."
after={
<Switch variant="Primary" value={callSummaryToast} onChange={setCallSummaryToast} />
@@ -2072,19 +2128,19 @@ function Calls() {
/>
{hapticsSupported() && (
<SettingTile
title="Haptic Feedback"
title="Haptic feedback"
description="A short vibration when push-to-talk engages or releases and when you send a reaction. Off automatically when your system prefers reduced motion."
after={<Switch variant="Primary" value={haptics} onChange={setHaptics} />}
/>
)}
<SettingTile
title="Push to Talk"
title="Push to talk"
description="Mute your microphone by default. Hold the PTT key to speak."
after={<Switch variant="Primary" value={pttMode} onChange={setPttMode} />}
/>
{pttMode && (
<SettingTile
title="PTT Key"
title="Push-to-talk key"
description={pttBind.error ?? 'Press a key to bind it as your push-to-talk key.'}
after={
<Button
@@ -2102,7 +2158,7 @@ function Calls() {
/>
)}
<SettingTile
title="Push to Deafen"
title="Push to deafen"
description={
deafenBind.error ??
(deafenHotkey
@@ -2143,7 +2199,7 @@ function Calls() {
/>
{isTauriEnv() && (
<SettingTile
title="Hotkeys Work Outside the Window"
title="Hotkeys work outside the window"
description="Keep Push to Talk and Push to Deafen working while a game or another app has focus. The keys are only watched during a call and are never blocked from other apps. Windows only; some anti-cheat systems may prevent it."
after={
<Switch variant="Primary" value={globalCallHotkeys} onChange={setGlobalCallHotkeys} />
@@ -2165,13 +2221,13 @@ function Calls() {
}
/>
<SettingTile
title="AFK Auto-Mute"
title="AFK auto-mute"
description="Automatically mute your microphone when you have been silent in a call."
after={<Switch variant="Primary" value={afkAutoMute} onChange={setAfkAutoMute} />}
/>
{afkAutoMute && (
<SettingTile
title="Idle Timeout"
title="Idle timeout"
description="How long to wait before auto-muting."
after={
<SettingsSelect
@@ -2203,7 +2259,7 @@ function Calls() {
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Ringtone Volume" description="Volume of the incoming call ringtone." />
<SettingTile title="Ringtone volume" description="Volume of the incoming call ringtone." />
<Box
direction="Row"
alignItems="Center"
@@ -2227,7 +2283,7 @@ function Calls() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Join & Leave Sounds"
title="Join and leave sounds"
description="Play a sound when someone joins or leaves a call you are in. Selecting an option plays a preview."
after={
<SettingsSelect
@@ -2246,7 +2302,7 @@ function Calls() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Microphone Bitrate"
title="Microphone bitrate"
description="Cap the audio bitrate your mic sends in calls. Lower saves bandwidth; higher is clearer. Auto lets Element Call decide."
after={
<SettingsSelect<CallAudioBitrate>
@@ -2260,7 +2316,7 @@ function Calls() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Screenshare Bitrate"
title="Screen share bitrate"
description="Cap the bitrate used when you share your screen. Lower is smoother on poor connections; higher is sharper."
after={
<SettingsSelect<ScreenshareBitrate>
@@ -2274,7 +2330,7 @@ function Calls() {
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Screenshare Framerate"
title="Screen share frame rate"
description="Cap the frames-per-second of your screenshare. 60 fps suits motion/gaming; 15 fps suits slides and saves bandwidth."
after={
<SettingsSelect<ScreenshareFramerate>
@@ -2300,7 +2356,7 @@ function Calls() {
/>
{soundboardEnabled && (
<>
<SettingTile title="Soundboard Volume" />
<SettingTile title="Soundboard volume" />
<Box
alignItems="Center"
gap="200"
@@ -2645,14 +2701,14 @@ function Messages() {
<Box direction="Column" gap="100">
<Text size="L400">Messages</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Message Layout" after={<SelectMessageLayout />} />
<SettingTile title="Message layout" after={<SelectMessageLayout />} />
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Message Spacing" after={<SelectMessageSpacing />} />
<SettingTile title="Message spacing" after={<SelectMessageSpacing />} />
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Legacy Username Color"
title="Legacy username colour"
after={
<Switch
variant="Primary"
@@ -2664,7 +2720,7 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide Membership Change"
title="Hide joins and leaves"
after={
<Switch
variant="Primary"
@@ -2676,7 +2732,7 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Hide Profile Change"
title="Hide name and avatar changes"
after={
<Switch
variant="Primary"
@@ -2688,7 +2744,7 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Disable Media Auto Load"
title="Don't load media automatically"
after={
<Switch
variant="Primary"
@@ -2700,14 +2756,14 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="URL Preview"
title="Link previews"
description="Your homeserver fetches and caches link previews. It will see the URLs of all links you preview."
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="URL Preview in Encrypted Rooms"
title="Link previews in encrypted rooms"
description={
<Box direction="Column" gap="100">
<Text size="T200" priority="300">
@@ -2731,34 +2787,29 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Inline Media Players"
title="Inline media players"
description="Play YouTube, Vimeo, TikTok, Twitch, Spotify and more right in the chat. Nothing loads from the third-party site until you press play."
after={
<Switch variant="Primary" value={inlineMediaEmbeds} onChange={setInlineMediaEmbeds} />
}
/>
</SequenceCard>
{/* [Gitea #457] Only where this build has a GIF key: a disabled switch
with a paragraph about missing configuration helps nobody. */}
{gifAvailable && (
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="GIF picker"
description="Every search term (and your IP address) is sent directly to Giphy, not through your homeserver. Off by default."
after={
<Switch variant="Primary" value={gifPickerEnabled} onChange={setGifPickerEnabled} />
}
/>
</SequenceCard>
)}
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="GIF Picker"
description={
gifAvailable
? 'Every search term (and your IP address) is sent directly to Giphy, not through your homeserver. Off by default.'
: "GIF search isn't set up for this version of the app (no Giphy key in its configuration), so there is no GIF button even when this is on."
}
after={
<Switch
variant="Primary"
value={gifPickerEnabled && gifAvailable}
onChange={setGifPickerEnabled}
disabled={!gifAvailable}
/>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Show Hidden Events"
title="Show hidden events"
after={
<Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
}
@@ -2766,7 +2817,7 @@ function Messages() {
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile
title="Enforce Message Retention"
title="Enforce message retention"
description="Permanently delete your own messages once a room's retention window (Room Settings → Message Retention) has passed. Off by default; only affects your own messages."
after={
<Switch
@@ -2783,7 +2834,7 @@ function Messages() {
direction="Column"
>
<SettingTile
title="Translate Messages Into"
title="Translate messages into"
description={
translationSupported
? 'Translate messages written in other languages into this one. Runs entirely on your device — message text never leaves your computer or touches a cloud service.'
@@ -2801,7 +2852,7 @@ function Messages() {
/>
{translationSupported && (
<SettingTile
title="Auto-translate Incoming Messages"
title="Auto-translate incoming messages"
description="Automatically translate messages that aren’t already in your language. When off, translate individual messages from their menu. The first time a language is used, its model downloads once (a few MB)."
after={<Switch variant="Primary" value={autoTranslate} onChange={setAutoTranslate} />}
/>
@@ -2962,9 +3013,9 @@ function AppUpdates() {
return (
<Box direction="Column" gap="100">
<Text size="L400">App Updates</Text>
<Text size="L400">App updates</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<SettingTile title="Check for Updates" description={description} after={after} />
<SettingTile title="Check for updates" description={description} after={after} />
{packageUpdate && packageHelp && <PackageUpdate help={packageHelp} />}
{status.state === 'error' && !packageUpdate && (
<Text size="T200" priority="300" style={{ wordBreak: 'break-word' }}>
@@ -82,7 +82,7 @@ export function AllMessagesNotifications() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">All Messages</Text>
<Text size="L400">All messages</Text>
<Box gap="100">
<Text size="T200">Badge: </Text>
<Badge radii="300" variant="Secondary" fill="Solid">
@@ -97,7 +97,7 @@ export function AllMessagesNotifications() {
gap="400"
>
<SettingTile
title="1-to-1 Chats"
title="1-to-1 chats"
after={<AllMessagesModeSwitcher pushRules={pushRules} ruleId={RuleId.DM} oneToOne />}
/>
</SequenceCard>
@@ -108,7 +108,7 @@ export function AllMessagesNotifications() {
gap="400"
>
<SettingTile
title="1-to-1 Chats (Encrypted)"
title="1-to-1 chats (encrypted)"
after={
<AllMessagesModeSwitcher
pushRules={pushRules}
@@ -137,7 +137,7 @@ export function AllMessagesNotifications() {
gap="400"
>
<SettingTile
title="Rooms (Encrypted)"
title="Rooms (encrypted)"
after={
<AllMessagesModeSwitcher
pushRules={pushRules}
@@ -171,7 +171,7 @@ export function KeywordMessagesNotifications() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">Keyword Messages</Text>
<Text size="L400">Keyword messages</Text>
<Box gap="100">
<Text size="T200">Badge: </Text>
<Badge radii="300" variant="Success" fill="Solid">
@@ -186,7 +186,7 @@ export function KeywordMessagesNotifications() {
gap="400"
>
<SettingTile
title="Select Keyword"
title="Add a keyword"
description="Set a notification preference for message containing given keyword."
>
<KeywordInput />
@@ -9,7 +9,6 @@ import { KeywordMessagesNotifications } from './KeywordMessages';
import { PushRuleEditor } from './PushRuleEditor';
import { SequenceCard } from '../../../components/sequence-card';
import { SequenceCardStyle } from '../styles.css';
import { SettingTile } from '../../../components/setting-tile';
import { settingsAtom, Settings } from '../../../state/settings';
import { notificationSnoozeUntilAtom } from '../../../state/notificationSnooze';
@@ -81,7 +80,7 @@ function NotificationPresets() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Quick Presets</Text>
<Text size="L400">Quick presets</Text>
<SequenceCard className={SequenceCardStyle} variant="SurfaceVariant" direction="Column">
<Box gap="300" style={{ padding: config.space.S300, flexWrap: 'wrap' }}>
{PRESETS.map((preset) => (
@@ -150,17 +149,6 @@ export function Notifications({ requestClose }: NotificationsProps) {
<SpecialMessagesNotifications />
<KeywordMessagesNotifications />
<PushRuleEditor />
<Box direction="Column" gap="100">
<Text size="L400">Block Messages</Text>
<SequenceCard
className={SequenceCardStyle}
variant="SurfaceVariant"
direction="Column"
gap="400"
>
<SettingTile description='This option has been moved to "Account > Block Users" section.' />
</SequenceCard>
</Box>
</Box>
</PageContent>
</Scroll>
@@ -333,10 +333,10 @@ export function PushRuleEditor() {
return (
<Box direction="Column" gap="100">
<Text size="L400">Advanced Push Rules</Text>
<RuleSection title="Override Rules" kind={PushRuleKind.Override} rules={overrideRules} />
<Text size="L400">Advanced push rules</Text>
<RuleSection title="Override rules" kind={PushRuleKind.Override} rules={overrideRules} />
<RuleSection
title="Room Rules"
title="Room rules"
kind={PushRuleKind.RoomSpecific}
rules={roomRules}
addForm={
@@ -348,7 +348,7 @@ export function PushRuleEditor() {
}
/>
<RuleSection
title="Sender Rules"
title="Sender rules"
kind={PushRuleKind.SenderSpecific}
rules={senderRules}
addForm={
@@ -359,7 +359,7 @@ export function PushRuleEditor() {
/>
}
/>
<RuleSection title="Underride Rules" kind={PushRuleKind.Underride} rules={underrideRules} />
<RuleSection title="Underride rules" kind={PushRuleKind.Underride} rules={underrideRules} />
</Box>
);
}
@@ -124,7 +124,7 @@ export function SpecialMessagesNotifications() {
return (
<Box direction="Column" gap="100">
<Box alignItems="Center" justifyContent="SpaceBetween" gap="200">
<Text size="L400">Special Messages</Text>
<Text size="L400">Special messages</Text>
<Box gap="100">
<Text size="T200">Badge: </Text>
<Badge radii="300" variant="Success" fill="Solid">
@@ -63,7 +63,7 @@ function EmailNotification() {
return (
<SettingTile
title="Email Notification"
title="Email notifications"
description={
<>
{result && !result.email && (
@@ -161,7 +161,7 @@ function PauseNotifications() {
return (
<SettingTile
title="Pause Notifications"
title="Pause notifications"
description={<span style={active ? { color: color.Warning.Main } : undefined}>{status}</span>}
after={
active ? (
@@ -221,7 +221,7 @@ export function SystemNotification() {
gap="400"
>
<SettingTile
title="Desktop Notifications"
title="Desktop notifications"
description={
notifPermission === 'denied' ? (
<Text as="span" style={{ color: color.Critical.Main }} size="T200">
@@ -230,7 +230,7 @@ export function SystemNotification() {
: 'Notifications are not supported by the system.'}
</Text>
) : (
<span>Show desktop notifications when message arrive.</span>
<span>Show desktop notifications when messages arrive.</span>
)
}
after={
@@ -263,8 +263,8 @@ export function SystemNotification() {
gap="400"
>
<SettingTile
title="Notification Sound"
description="Play sound when new message arrive."
title="Notification sound"
description="Play a sound when a new message arrives."
after={<Switch value={isNotificationSounds} onChange={setIsNotificationSounds} />}
/>
{isNotificationSounds && (
@@ -353,7 +353,7 @@ export function SystemNotification() {
gap="400"
>
<SettingTile
title="Quiet Hours"
title="Quiet hours"
description="Notifications are silenced between these times. Handles overnight (e.g. 11:00 PM – 8:00 AM)."
after={<Switch value={quietHoursEnabled} onChange={setQuietHoursEnabled} />}
/>