import React, { ChangeEventHandler, FormEventHandler, KeyboardEventHandler, MouseEventHandler, useEffect, useState, } from 'react'; import dayjs from 'dayjs'; import { as, Box, Button, Chip, config, Icon, IconButton, Icons, Input, Menu, MenuItem, PopOut, RectCords, Scroll, Switch, Text, toRem, } from 'folds'; import { isKeyHotkey } from 'is-hotkey'; import FocusTrap from 'focus-trap-react'; import { Page, PageContent, PageHeader } from '../../../components/page'; import { SequenceCard } from '../../../components/sequence-card'; import { useSetting } from '../../../state/hooks/settings'; import { DateFormat, MessageLayout, MessageSpacing, settingsAtom } from '../../../state/settings'; import { SettingTile } from '../../../components/setting-tile'; import { KeySymbol } from '../../../utils/key-symbol'; import { isMacOS } from '../../../utils/user-agent'; import { DarkTheme, LightTheme, Theme, ThemeKind, useSystemThemeKind, useThemeNames, useThemes, } from '../../../hooks/useTheme'; import { stopPropagation } from '../../../utils/keyboard'; import { useMessageLayoutItems } from '../../../hooks/useMessageLayout'; import { useMessageSpacingItems } from '../../../hooks/useMessageSpacing'; import { useDateFormatItems } from '../../../hooks/useDateFormat'; import { SequenceCardStyle } from '../styles.css'; 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, }} > } /> ); } 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 [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji'); return ( Appearance } /> {systemTheme && } } /> } /> } /> ); } type CustomDateFormatProps = { dateFormatString: string; setDateFormatString: (format: string) => void; }; function CustomDateFormat({ dateFormatString, setDateFormatString }: CustomDateFormatProps) { const [dateFormatCustom, setDateFormatCustom] = useState(dateFormatString); useEffect(() => { setDateFormatCustom(dateFormatString); }, [dateFormatString]); const handleChange: ChangeEventHandler = (evt) => { const name = evt.currentTarget.value; setDateFormatCustom(name); }; const handleReset = () => { setDateFormatCustom(dateFormatString); }; 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; setDateFormatString(format); }; const hasChanges = dateFormatCustom !== dateFormatString; return ( View formatting syntax on{' '} Day.js . } > ) } /> ); } type PresetDateFormatProps = { dateFormatPreset: string; handlePresetChange: (format: string) => void; }; function PresetDateFormat({ dateFormatPreset, handlePresetChange }: PresetDateFormatProps) { const [menuCords, setMenuCords] = useState(); const dateFormatItems = useDateFormatItems(); const handleMenu: MouseEventHandler = (evt) => { setMenuCords(evt.currentTarget.getBoundingClientRect()); }; const handleSelect = (format: DateFormat) => { handlePresetChange(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)} > {item.name} ))} } /> ); } function SelectDateFormat() { const [dateFormatString, setDateFormatString] = useSetting(settingsAtom, 'dateFormatString'); const [dateFormatPreset, setDateFormatPreset] = useState(dateFormatString); const handlePresetChange = (format: string) => { setDateFormatPreset(format); if (format !== '') { setDateFormatString(format); } }; return ( <> } /> {dateFormatPreset === '' && ( )} ); } 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 [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity'); return ( Editor } /> } /> } /> ); } function SelectMessageLayout() { const [menuCords, setMenuCords] = useState(); const [messageLayout, setMessageLayout] = useSetting(settingsAtom, 'messageLayout'); const messageLayoutItems = useMessageLayoutItems(); const handleMenu: MouseEventHandler = (evt) => { setMenuCords(evt.currentTarget.getBoundingClientRect()); }; const handleSelect = (layout: MessageLayout) => { setMessageLayout(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, }} > {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)} /> } /> } /> } /> } /> ); } type GeneralProps = { requestClose: () => void; }; export function General({ requestClose }: GeneralProps) { return ( General ); }