import React, { ChangeEventHandler, FormEventHandler, KeyboardEventHandler, MouseEventHandler, useCallback, useEffect, useRef, useState, } from 'react'; import dayjs from 'dayjs'; import { as, Box, Button, Chip, color, config, Header, Icon, IconButton, Icons, Input, Menu, MenuItem, PopOut, RectCords, Scroll, Spinner, Switch, Text, toRem, } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; import { HexColorPicker } from 'react-colorful'; import FocusTrap from 'focus-trap-react'; import { HexColorPickerPopOut } from '../../../components/HexColorPickerPopOut'; import { BgSwatch as BgSwatchStyle } from './BgSwatch.css'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { SequenceCard } from '../../../components/sequence-card'; import { DENOISE_MODELS, isMLDenoiseSupported, ML_DENOISE_REQUIREMENTS, } from '../../../utils/lotusDenoiseUtils'; import { useSetting } from '../../../state/hooks/settings'; import { ChatBackground, ComposerToolbarSettings, DateFormat, DenoiseModelId, MessageLayout, MessageSpacing, NoiseSuppressionMode, RingtoneId, Settings, settingsAtom, } from '../../../state/settings'; import { SeasonalPreview, SeasonTheme } from '../../../components/seasonal/SeasonalEffect'; import { SettingTile } from '../../../components/setting-tile'; import { KeySymbol } from '../../../utils/key-symbol'; import { isMacOS } from '../../../utils/user-agent'; import { DarkTheme, LightTheme, Theme, ThemeKind, useSystemThemeKind, useTheme, useThemeNames, useThemes, } from '../../../hooks/useTheme'; import { stopPropagation } from '../../../utils/keyboard'; import { BG_OPTIONS, getChatBg } from '../../lotus/chatBackground'; import { resetBootSequence, runLotusBootSequence } from '../../../../lotus-boot'; import { useMessageLayoutItems } from '../../../hooks/useMessageLayout'; import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing'; import { SequenceCardStyle } from '../styles.css'; import { useTauriUpdater } from '../../../hooks/useTauriUpdater'; import { useDateFormatItems } from '../../../hooks/useDateFormat'; import { playCallJoinSound } from '../../../utils/callSounds'; import { previewRingtone, RINGTONE_OPTIONS } from '../../../utils/ringtones'; import { DenoiseTester } from './DenoiseTester'; type ThemeSelectorProps = { themeNames: Record; themes: Theme[]; selected: Theme; onSelect: (theme: Theme) => void; }; const ThemeSelector = as<'div', ThemeSelectorProps>( ({ themeNames, themes, selected, onSelect, ...props }, ref) => ( {themes.map((theme) => ( onSelect(theme)} > {themeNames[theme.id] ?? theme.id} ))} ), ); function SelectTheme({ disabled }: { disabled?: boolean }) { const themes = useThemes(); const themeNames = useThemeNames(); const [themeId, setThemeId] = useSetting(settingsAtom, 'themeId'); const [menuCords, setMenuCords] = useState(); const selectedTheme = themes.find((theme) => theme.id === themeId) ?? LightTheme; const handleThemeMenu: MouseEventHandler = (evt) => { setMenuCords(evt.currentTarget.getBoundingClientRect()); }; const handleThemeSelect = (theme: Theme) => { setThemeId(theme.id); setMenuCords(undefined); }; return ( <> setMenuCords(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown' || evt.key === 'ArrowRight', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', escapeDeactivates: stopPropagation, }} > } /> ); } type SettingsSelectOption = { value: T; label: string; disabled?: boolean }; function SettingsSelect({ value, options, onChange, }: { value: T; options: SettingsSelectOption[]; onChange: (v: T) => void; }) { const [menuCords, setMenuCords] = useState(); const selectedLabel = options.find((o) => o.value === value)?.label ?? value; const handleMenu: MouseEventHandler = (evt) => { setMenuCords(evt.currentTarget.getBoundingClientRect()); }; const handleSelect = (v: T) => { onChange(v); setMenuCords(undefined); }; return ( <> setMenuCords(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown' || evt.key === 'ArrowRight', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', escapeDeactivates: stopPropagation, }} > {options.map((opt) => ( !opt.disabled && handleSelect(opt.value)} > {opt.label} ))} } /> ); } function SystemThemePreferences() { const themeKind = useSystemThemeKind(); const themeNames = useThemeNames(); const themes = useThemes(); const [lightThemeId, setLightThemeId] = useSetting(settingsAtom, 'lightThemeId'); const [darkThemeId, setDarkThemeId] = useSetting(settingsAtom, 'darkThemeId'); const lightThemes = themes.filter((theme) => theme.kind === ThemeKind.Light); const darkThemes = themes.filter((theme) => theme.kind === ThemeKind.Dark); const selectedLightTheme = lightThemes.find((theme) => theme.id === lightThemeId) ?? LightTheme; const selectedDarkTheme = darkThemes.find((theme) => theme.id === darkThemeId) ?? DarkTheme; const [ltCords, setLTCords] = useState(); const [dtCords, setDTCords] = useState(); const handleLightThemeMenu: MouseEventHandler = (evt) => { setLTCords(evt.currentTarget.getBoundingClientRect()); }; const handleDarkThemeMenu: MouseEventHandler = (evt) => { setDTCords(evt.currentTarget.getBoundingClientRect()); }; const handleLightThemeSelect = (theme: Theme) => { setLightThemeId(theme.id); setLTCords(undefined); }; const handleDarkThemeSelect = (theme: Theme) => { setDarkThemeId(theme.id); setDTCords(undefined); }; return ( } onClick={handleLightThemeMenu} > {themeNames[selectedLightTheme.id] ?? selectedLightTheme.id} } /> setLTCords(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown' || evt.key === 'ArrowRight', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', escapeDeactivates: stopPropagation, }} > } /> } onClick={handleDarkThemeMenu} > {themeNames[selectedDarkTheme.id] ?? selectedDarkTheme.id} } /> setDTCords(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown' || evt.key === 'ArrowRight', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', escapeDeactivates: stopPropagation, }} > } /> ); } function PageZoomInput() { const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom'); const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`); const handleZoomChange: ChangeEventHandler = (evt) => { setCurrentZoom(evt.target.value); }; const handleZoomEnter: KeyboardEventHandler = (evt) => { if (isKeyHotkey('escape', evt)) { evt.stopPropagation(); setCurrentZoom(pageZoom.toString()); } if ( isKeyHotkey('enter', evt) && 'value' in evt.target && typeof evt.target.value === 'string' ) { const newZoom = parseInt(evt.target.value, 10); if (Number.isNaN(newZoom)) return; const safeZoom = Math.max(Math.min(newZoom, 150), 75); setPageZoom(safeZoom); setCurrentZoom(safeZoom.toString()); } }; return ( %} outlined /> ); } function Appearance() { const [systemTheme, setSystemTheme] = useSetting(settingsAtom, 'useSystemTheme'); const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode'); const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); const [perMessageProfiles, setPerMessageProfiles] = useSetting( settingsAtom, 'perMessageProfiles', ); const [lotusTerminal, setLotusTerminal] = useSetting(settingsAtom, 'lotusTerminal'); const [nightLightEnabled, setNightLightEnabled] = useSetting(settingsAtom, 'nightLightEnabled'); const [nightLightOpacity, setNightLightOpacity] = useSetting(settingsAtom, 'nightLightOpacity'); const [glassmorphismSidebar, setGlassmorphismSidebar] = useSetting( settingsAtom, 'glassmorphismSidebar', ); const [pauseAnimations, setPauseAnimations] = useSetting(settingsAtom, 'pauseAnimations'); const [mentionHighlightColor, setMentionHighlightColor] = useSetting( settingsAtom, 'mentionHighlightColor', ); const [fontFamily, setFontFamily] = useSetting(settingsAtom, 'fontFamily'); const [seasonalThemeOverride, setSeasonalThemeOverride] = useSetting( settingsAtom, 'seasonalThemeOverride', ); const [, setChatBackground] = useSetting(settingsAtom, 'chatBackground'); return ( Appearance } /> {systemTheme && } } /> } /> } /> } /> } /> { setSeasonalThemeOverride(v); if (v !== 'auto' && v !== 'off') setChatBackground('none'); }} /> } /> } /> {nightLightEnabled && ( Intensity: {nightLightOpacity}% ) => setNightLightOpacity(parseInt(e.target.value, 10)) } style={{ width: '100%', accentColor: color.Primary.Main }} /> )} } /> {lotusTerminal && ( )} } /> setFontFamily(v as 'system' | 'inter' | 'jetbrains-mono' | 'fira-code') } options={[ { value: 'system', label: 'System Default' }, { value: 'inter', label: 'Inter (default)' }, { value: 'jetbrains-mono', label: 'JetBrains Mono' }, { value: 'fira-code', label: 'Fira Code' }, ]} /> } /> } onRemove={mentionHighlightColor ? () => setMentionHighlightColor('') : undefined} > {(openPicker, opened) => ( )} } /> ); } type DateHintProps = { hasChanges: boolean; handleReset: () => void; }; function DateHint({ hasChanges, handleReset }: DateHintProps) { const [anchor, setAnchor] = useState(); const categoryPadding = { padding: config.space.S200, paddingTop: 0 }; const handleOpenMenu: MouseEventHandler = (evt) => { setAnchor(evt.currentTarget.getBoundingClientRect()); }; return ( setAnchor(undefined), clickOutsideDeactivates: true, escapeDeactivates: stopPropagation, }} >
Formatting
Year
YY {': '} Two-digit year {' '} YYYY {': '}Four-digit year
Month
M {': '}The month MM {': '}Two-digit month {' '} MMM {': '}Short month name MMMM {': '}Full month name
Day of the Month
D {': '}Day of the month DD {': '}Two-digit day of the month
Day of the Week
d {': '}Day of the week (Sunday = 0) dd {': '}Two-letter day name ddd {': '}Short day name dddd {': '}Full day name
} > {hasChanges ? ( ) : ( )}
); } type CustomDateFormatProps = { value: string; onChange: (format: string) => void; }; function CustomDateFormat({ value, onChange }: CustomDateFormatProps) { const [dateFormatCustom, setDateFormatCustom] = useState(value); useEffect(() => { setDateFormatCustom(value); }, [value]); const handleChange: ChangeEventHandler = (evt) => { const format = evt.currentTarget.value; setDateFormatCustom(format); }; const handleReset = () => { setDateFormatCustom(value); }; const handleSubmit: FormEventHandler = (evt) => { evt.preventDefault(); const target = evt.target as HTMLFormElement | undefined; const customDateFormatInput = target?.customDateFormatInput as HTMLInputElement | undefined; const format = customDateFormatInput?.value; if (!format) return; onChange(format); }; const hasChanges = dateFormatCustom !== value; return ( } /> ); } type PresetDateFormatProps = { value: string; onChange: (format: string) => void; }; function PresetDateFormat({ value, onChange }: PresetDateFormatProps) { const [menuCords, setMenuCords] = useState(); const dateFormatItems = useDateFormatItems(); const getDisplayDate = (format: string): string => format !== '' ? dayjs().format(format) : 'Custom'; const handleMenu: MouseEventHandler = (evt) => { setMenuCords(evt.currentTarget.getBoundingClientRect()); }; const handleSelect = (format: DateFormat) => { onChange(format); setMenuCords(undefined); }; return ( <> setMenuCords(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown' || evt.key === 'ArrowRight', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', escapeDeactivates: stopPropagation, }} > {dateFormatItems.map((item) => ( handleSelect(item.format)} > {getDisplayDate(item.format)} ))} } /> ); } function SelectDateFormat() { const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const [selectedDateFormat, setSelectedDateFormat] = useState(dateFormatString); const customDateFormat = selectedDateFormat === ''; const handlePresetChange = (format: string) => { setSelectedDateFormat(format); if (format !== '') { setDateFormatString(format); } }; return ( <> } /> {customDateFormat && ( )} ); } function DateAndTime() { const [hour24Clock, setHour24Clock] = useSetting(settingsAtom, 'hour24Clock'); return ( Date & Time } /> ); } function Editor() { const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline'); const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown'); const [editorToolbar, setEditorToolbar] = useSetting(settingsAtom, 'editorToolbar'); const [composerToolbarButtons, setComposerToolbarButtons] = useSetting( settingsAtom, 'composerToolbarButtons', ); const toggleToolbarButton = (key: keyof ComposerToolbarSettings) => { setComposerToolbarButtons({ ...composerToolbarButtons, [key]: !composerToolbarButtons[key] }); }; const TOOLBAR_CHIPS: Array<{ key: keyof ComposerToolbarSettings; label: string }> = [ { key: 'showFormat', label: 'Format' }, { key: 'showEmoji', label: 'Emoji' }, { key: 'showSticker', label: 'Sticker' }, { key: 'showGif', label: 'GIF' }, { key: 'showLocation', label: 'Location' }, { key: 'showPoll', label: 'Poll' }, { key: 'showVoice', label: 'Voice' }, { key: 'showSchedule', label: 'Schedule' }, ]; return ( Editor } /> } /> } /> {TOOLBAR_CHIPS.map(({ key, label }) => { const active = composerToolbarButtons?.[key] ?? true; return ( toggleToolbarButton(key)} aria-pressed={active} > {label} ); })} ); } function Privacy() { const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity'); const [hidePresence, setHidePresence] = useSetting(settingsAtom, 'hidePresence'); const [privateReadReceipts, setPrivateReadReceipts] = useSetting( settingsAtom, 'privateReadReceipts', ); const [warnOnUnverifiedDevices, setWarnOnUnverifiedDevices] = useSetting( settingsAtom, 'warnOnUnverifiedDevices', ); return ( Privacy } /> } /> } /> } /> ); } function useKeyBind(setter: (code: string) => void) { const [listening, setListening] = useState(false); const listenerRef = useRef<((e: KeyboardEvent) => void) | null>(null); useEffect( () => () => { if (listenerRef.current) window.removeEventListener('keydown', listenerRef.current, true); }, [], ); const startListening = useCallback(() => { if (listening) return; setListening(true); const onKey = (e: KeyboardEvent) => { e.preventDefault(); if (e.code !== 'Escape') setter(e.code); setListening(false); window.removeEventListener('keydown', onKey, true); listenerRef.current = null; }; listenerRef.current = onKey; window.addEventListener('keydown', onKey, true); }, [listening, setter]); return { listening, startListening }; } const keyLabel = (code: string) => code === 'Space' ? 'Space' : code.replace('Key', '').replace('Digit', ''); function Calls() { const [cameraOnJoin, setCameraOnJoin] = useSetting(settingsAtom, 'cameraOnJoin'); const [callNoiseSuppression, setCallNoiseSuppression] = useSetting( settingsAtom, 'callNoiseSuppression', ); const [callDenoiseModel, setCallDenoiseModel] = useSetting(settingsAtom, 'callDenoiseModel'); const [callDenoiseNativeNS, setCallDenoiseNativeNS] = useSetting( settingsAtom, 'callDenoiseNativeNS', ); const [callDenoiseGate, setCallDenoiseGate] = useSetting(settingsAtom, 'callDenoiseGate'); const [callDenoiseGateThreshold, setCallDenoiseGateThreshold] = useSetting( settingsAtom, 'callDenoiseGateThreshold', ); const [pttMode, setPttMode] = useSetting(settingsAtom, 'pttMode'); const [pttKey, setPttKey] = useSetting(settingsAtom, 'pttKey'); const [deafenKey, setDeafenKey] = useSetting(settingsAtom, 'deafenKey'); const [afkAutoMute, setAfkAutoMute] = useSetting(settingsAtom, 'afkAutoMute'); const [afkTimeoutMinutes, setAfkTimeoutMinutes] = useSetting(settingsAtom, 'afkTimeoutMinutes'); const [callJoinLeaveSound, setCallJoinLeaveSound] = useSetting( settingsAtom, 'callJoinLeaveSound', ); const [ringtoneVolume, setRingtoneVolume] = useSetting(settingsAtom, 'ringtoneVolume'); const [ringtoneId, setRingtoneId] = useSetting(settingsAtom, 'ringtoneId'); const handleJoinLeaveSoundChange = (value: 'off' | 'chime' | 'soft' | 'retro') => { setCallJoinLeaveSound(value); if (value !== 'off') playCallJoinSound(value); }; const handleRingtoneChange = (value: RingtoneId) => { setRingtoneId(value); previewRingtone(value, Math.max(0, Math.min(1, ringtoneVolume / 100))); }; const pttBind = useKeyBind(setPttKey); const deafenBind = useKeyBind(setDeafenKey); const mlSupported = isMLDenoiseSupported(); const selectedDenoiseModel = DENOISE_MODELS.find((m) => m.id === callDenoiseModel); return ( Calls } /> Filter background noise from your mic during calls. Browser-native uses the built-in WebRTC suppressor (Google NSNet2). ML runs a dedicated model for stronger removal. {!mlSupported && ( ML options are not supported in this browser. {ML_DENOISE_REQUIREMENTS.map((req) => ( {req} ))} )} } after={ value={callNoiseSuppression} onChange={setCallNoiseSuppression} options={[ { value: 'off', label: 'Off' }, { value: 'browser', label: 'Browser-native' }, { value: 'ml', label: 'ML (Advanced)', disabled: !mlSupported, }, ]} /> } /> {callNoiseSuppression === 'ml' && ( {/* ── Model selection ───────────────────────────────────────── */} Model value={callDenoiseModel} onChange={setCallDenoiseModel} options={DENOISE_MODELS.map((m) => ({ value: m.id, label: m.name }))} /> } /> {selectedDenoiseModel && ( {selectedDenoiseModel.name} {selectedDenoiseModel.description} {( [ { label: 'CPU', value: selectedDenoiseModel.cpuUsage }, { label: 'Voice quality', value: selectedDenoiseModel.voiceQuality }, { label: 'Transients', value: selectedDenoiseModel.transients }, { label: 'Download', value: selectedDenoiseModel.binarySize }, ] as const ).map((stat) => ( {stat.label} {stat.value} ))} )}
Compare all models Model CPU Quality Transients {DENOISE_MODELS.map((model) => ( {model.name} {model.cpuUsage} {model.voiceQuality} {model.transients} ))}
Note: Applying changes requires rejoining the call.
{/* ── Enhancement toggles ───────────────────────────────────── */} Enhancements } /> } /> {callDenoiseGate && ( Gate Threshold {callDenoiseGateThreshold} dB setCallDenoiseGateThreshold(parseInt(e.target.value, 10))} style={{ width: '100%', accentColor: 'var(--accent-orange)' }} /> )} {/* ── Test & calibrate ──────────────────────────────────────── */} Test & calibrate 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.
)} } /> {pttMode && ( {pttBind.listening ? 'Press a key…' : keyLabel(pttKey)} } /> )} {deafenBind.listening ? 'Press a key…' : keyLabel(deafenKey)} } /> } /> {afkAutoMute && ( setAfkTimeoutMinutes(Number(v))} options={[ { value: '1', label: '1 minute' }, { value: '5', label: '5 minutes' }, { value: '10', label: '10 minutes' }, { value: '20', label: '20 minutes' }, { value: '30', label: '30 minutes' }, ]} /> } /> )} handleRingtoneChange(v as RingtoneId)} options={RINGTONE_OPTIONS} /> } /> setRingtoneVolume(parseInt(e.target.value, 10))} aria-label="Ringtone volume" style={{ flex: 1, accentColor: 'var(--accent-orange)' }} /> {ringtoneVolume}% } /> handleJoinLeaveSoundChange(v as 'off' | 'chime' | 'soft' | 'retro')} options={[ { value: 'off', label: 'Off' }, { value: 'chime', label: 'Chime' }, { value: 'soft', label: 'Soft' }, { value: 'retro', label: 'Retro' }, ]} /> } /> ); } const SEASONAL_OPTIONS: { value: Settings['seasonalThemeOverride']; label: string; emoji: string; }[] = [ { value: 'auto', label: 'Auto', emoji: '🗓' }, { value: 'off', label: 'Off', emoji: '×' }, { value: 'newyear', label: 'New Year', emoji: '🎆' }, { value: 'lunar', label: 'Lunar New Year', emoji: '🏮' }, { value: 'valentines', label: "Valentine's", emoji: '💖' }, { value: 'stpatricks', label: "St. Patrick's", emoji: '🍀' }, { value: 'aprilfools', label: 'April Fools', emoji: '?' }, { value: 'earthday', label: 'Earth Day', emoji: '🌱' }, { value: 'autumn', label: 'Autumn', emoji: '🍂' }, { value: 'halloween', label: 'Halloween', emoji: '🎃' }, { value: 'christmas', label: 'Christmas', emoji: '❄️' }, { value: 'arcade', label: 'Arcade Day', emoji: '👾' }, { value: 'deepspace', label: 'Deep Space', emoji: '🚀' }, ]; function SeasonalBgGrid({ value, onChange, }: { value: Settings['seasonalThemeOverride']; onChange: (v: Settings['seasonalThemeOverride']) => void; }) { return ( {SEASONAL_OPTIONS.map((opt) => { const selected = value === opt.value; const isSpecial = opt.value === 'auto' || opt.value === 'off'; return ( {opt.label} ); })} ); } function ChatBgGrid() { const [chatBackground, setChatBackground] = useSetting(settingsAtom, 'chatBackground'); const [, setSeasonalThemeOverride] = useSetting(settingsAtom, 'seasonalThemeOverride'); const [pauseAnimations] = useSetting(settingsAtom, 'pauseAnimations'); const theme = useTheme(); const isDark = theme.kind === ThemeKind.Dark; return ( {BG_OPTIONS.map((opt) => ( setMenuCords(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown' || evt.key === 'ArrowRight', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', escapeDeactivates: stopPropagation, }} > {messageLayoutItems.map((item) => ( handleSelect(item.layout)} > {item.name} ))} } /> ); } function SelectMessageSpacing() { const [menuCords, setMenuCords] = useState(); const [messageSpacing, setMessageSpacing] = useSetting(settingsAtom, 'messageSpacing'); const messageSpacingItems = useMessageSpacingItems(); const handleMenu: MouseEventHandler = (evt) => { setMenuCords(evt.currentTarget.getBoundingClientRect()); }; const handleSelect = (layout: MessageSpacing) => { setMessageSpacing(layout); setMenuCords(undefined); }; return ( <> setMenuCords(undefined), clickOutsideDeactivates: true, isKeyForward: (evt: KeyboardEvent) => evt.key === 'ArrowDown' || evt.key === 'ArrowRight', isKeyBackward: (evt: KeyboardEvent) => evt.key === 'ArrowUp' || evt.key === 'ArrowLeft', escapeDeactivates: stopPropagation, }} > {messageSpacingItems.map((item) => ( handleSelect(item.spacing)} > {item.name} ))} } /> ); } function Messages() { const [legacyUsernameColor, setLegacyUsernameColor] = useSetting( settingsAtom, 'legacyUsernameColor', ); const [hideMembershipEvents, setHideMembershipEvents] = useSetting( settingsAtom, 'hideMembershipEvents', ); const [hideNickAvatarEvents, setHideNickAvatarEvents] = useSetting( settingsAtom, 'hideNickAvatarEvents', ); const [mediaAutoLoad, setMediaAutoLoad] = useSetting(settingsAtom, 'mediaAutoLoad'); const [urlPreview, setUrlPreview] = useSetting(settingsAtom, 'urlPreview'); const [encUrlPreview, setEncUrlPreview] = useSetting(settingsAtom, 'encUrlPreview'); const [showHiddenEvents, setShowHiddenEvents] = useSetting(settingsAtom, 'showHiddenEvents'); return ( Messages } /> } /> } /> } /> } /> setMediaAutoLoad(!v)} /> } /> } /> Your homeserver fetches link previews on your behalf. It cannot decrypt your messages, but will see every URL you preview in encrypted rooms, potentially revealing conversation topics. Privacy risk — enabled by default } after={} /> } /> ); } function AppUpdates() { const { isTauri, status, check, install } = useTauriUpdater(); if (!isTauri) return null; const description = status.state === 'checking' ? 'Checking for updates...' : status.state === 'up-to-date' ? 'Lotus Chat is up to date.' : status.state === 'available' ? `Update available: v${status.version}` : status.state === 'installing' ? 'Installing update, the app will restart shortly...' : status.state === 'error' ? `Update check failed: ${status.message}` : 'Check for a new version of Lotus Chat.'; const after = status.state === 'available' ? ( ) : status.state === 'checking' || status.state === 'installing' ? ( ) : ( ); return ( App Updates ); } type GeneralProps = { requestClose: () => void; }; export function General({ requestClose }: GeneralProps) { return ( General ); }