vojo/src/app/features/settings/general/General.tsx

260 lines
8.8 KiB
TypeScript

import React, { ChangeEventHandler, KeyboardEventHandler, useState } from 'react';
import {
Box,
Icon,
IconButton,
Icons,
Input,
Scroll,
Switch,
Text,
toRem,
} from 'folds';
import { isKeyHotkey } from 'is-hotkey';
import { useTranslation } from 'react-i18next';
import { Page, PageContent, PageHeader } from '../../../components/page';
import { SequenceCard } from '../../../components/sequence-card';
import { useSetting } from '../../../state/hooks/settings';
import { settingsAtom } from '../../../state/settings';
import { SettingTile } from '../../../components/setting-tile';
import { KeySymbol } from '../../../utils/key-symbol';
import { isMacOS } from '../../../utils/user-agent';
import { SequenceCardStyle } from '../styles.css';
function ThemeSelect() {
// Theme switching is locked to dark while the Dawn redesign rolls out — Vojo
// light is a separate plan. Kept as a read-only label so the existing
// SettingTile layout in the Appearance section still renders.
const { t } = useTranslation();
return (
<Text size="T300" priority="300">
{t('Settings.theme_dark')}
</Text>
);
}
function PageZoomInput() {
const [pageZoom, setPageZoom] = useSetting(settingsAtom, 'pageZoom');
const [currentZoom, setCurrentZoom] = useState(`${pageZoom}`);
const handleZoomChange: ChangeEventHandler<HTMLInputElement> = (evt) => {
setCurrentZoom(evt.target.value);
};
const handleZoomEnter: KeyboardEventHandler<HTMLInputElement> = (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 (
<Input
style={{ width: toRem(100) }}
variant={pageZoom === parseInt(currentZoom, 10) ? 'Secondary' : 'Success'}
size="300"
radii="300"
type="number"
min="75"
max="150"
value={currentZoom}
onChange={handleZoomChange}
onKeyDown={handleZoomEnter}
after={<Text size="T300">%</Text>}
outlined
/>
);
}
function Appearance() {
const { t } = useTranslation();
const [monochromeMode, setMonochromeMode] = useSetting(settingsAtom, 'monochromeMode');
const [twitterEmoji, setTwitterEmoji] = useSetting(settingsAtom, 'twitterEmoji');
return (
<Box direction="Column" gap="100">
<Text size="L400">{t('Settings.appearance')}</Text>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile title={t('Settings.theme')} after={<ThemeSelect />} />
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.monochrome_mode')}
after={<Switch variant="Primary" value={monochromeMode} onChange={setMonochromeMode} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.twitter_emoji')}
after={<Switch variant="Primary" value={twitterEmoji} onChange={setTwitterEmoji} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile title={t('Settings.page_zoom')} after={<PageZoomInput />} />
</SequenceCard>
</Box>
);
}
function Editor() {
const { t } = useTranslation();
const [enterForNewline, setEnterForNewline] = useSetting(settingsAtom, 'enterForNewline');
const [isMarkdown, setIsMarkdown] = useSetting(settingsAtom, 'isMarkdown');
const [hideActivity, setHideActivity] = useSetting(settingsAtom, 'hideActivity');
return (
<Box direction="Column" gap="100">
<Text size="L400">{t('Settings.editor')}</Text>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.enter_newline')}
description={t('Settings.enter_newline_desc', {
key: isMacOS() ? KeySymbol.Command : 'Ctrl',
})}
after={<Switch variant="Primary" value={enterForNewline} onChange={setEnterForNewline} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.markdown')}
after={<Switch variant="Primary" value={isMarkdown} onChange={setIsMarkdown} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.hide_activity')}
description={t('Settings.hide_activity_desc')}
after={<Switch variant="Primary" value={hideActivity} onChange={setHideActivity} />}
/>
</SequenceCard>
</Box>
);
}
function Messages() {
const { t } = useTranslation();
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 (
<Box direction="Column" gap="100">
<Text size="L400">{t('Settings.messages')}</Text>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.hide_membership')}
after={
<Switch
variant="Primary"
value={hideMembershipEvents}
onChange={setHideMembershipEvents}
/>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.hide_profile')}
after={
<Switch
variant="Primary"
value={hideNickAvatarEvents}
onChange={setHideNickAvatarEvents}
/>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.disable_media_auto_load')}
after={
<Switch
variant="Primary"
value={!mediaAutoLoad}
onChange={(v) => setMediaAutoLoad(!v)}
/>
}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.url_preview')}
after={<Switch variant="Primary" value={urlPreview} onChange={setUrlPreview} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.url_preview_encrypted')}
after={<Switch variant="Primary" value={encUrlPreview} onChange={setEncUrlPreview} />}
/>
</SequenceCard>
<SequenceCard className={SequenceCardStyle} variant="Background" direction="Column">
<SettingTile
title={t('Settings.show_hidden_events')}
after={
<Switch variant="Primary" value={showHiddenEvents} onChange={setShowHiddenEvents} />
}
/>
</SequenceCard>
</Box>
);
}
type GeneralProps = {
requestClose: () => void;
};
export function General({ requestClose }: GeneralProps) {
const { t } = useTranslation();
return (
<Page variant="SurfaceVariant">
<PageHeader outlined={false}>
<Box grow="Yes" gap="200">
<Box grow="Yes" alignItems="Center" gap="200">
<Text size="H3" truncate>
{t('Settings.general_title')}
</Text>
</Box>
<Box shrink="No">
<IconButton onClick={requestClose} variant="SurfaceVariant" aria-label={t('Settings.close')}>
<Icon src={Icons.Cross} />
</IconButton>
</Box>
</Box>
</PageHeader>
<Box grow="Yes">
<Scroll hideTrack visibility="Hover">
<PageContent style={{ paddingBottom: '1rem' }}>
<Box direction="Column" gap="700">
<Appearance />
<Editor />
<Messages />
</Box>
</PageContent>
</Scroll>
</Box>
</Page>
);
}