mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 08:07:29 +00:00
feat: add an accessibility page with presets, larger text sizes and a notification duration
This commit is contained in:
@@ -0,0 +1,203 @@
|
||||
import React from 'react';
|
||||
import { View } from 'react-native';
|
||||
import { SettingsSection, SettingItem, Select, ToggleSwitch } from './settings-section';
|
||||
import Button from '../Button';
|
||||
import { spacing } from '../../theme/tokens';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { FONT_SIZES, useSettingsStore, type FontSize } from '../../stores/settings-store';
|
||||
import { toast } from '../../stores/toast-store';
|
||||
import {
|
||||
ACCESSIBILITY_PRESET_IDS,
|
||||
applyPresetChanges,
|
||||
presetChanges,
|
||||
undoPresetChanges,
|
||||
type AccessibilityPresetId,
|
||||
} from '../../lib/accessibility-presets';
|
||||
import { TOAST_DURATION_CHOICES } from '../../lib/toast-duration';
|
||||
|
||||
// Percentages read the same in every language the app has.
|
||||
const TEXT_SIZE_LABELS: Record<FontSize, string> = {
|
||||
small: '87.5%',
|
||||
medium: '100%',
|
||||
large: '112.5%',
|
||||
'x-large': '125%',
|
||||
'xx-large': '150%',
|
||||
'xxx-large': '175%',
|
||||
huge: '200%',
|
||||
};
|
||||
|
||||
type Translate = (key: string, fallback?: string, params?: Record<string, string | number>) => string;
|
||||
|
||||
// Literal keys, so the harvest script and the key test see them.
|
||||
function presetCopy(id: AccessibilityPresetId, t: Translate): { label: string; description: string } {
|
||||
switch (id) {
|
||||
case 'screen-reader':
|
||||
return {
|
||||
label: t('settings.accessibility.presets.screen-reader.label', 'Screen reader'),
|
||||
description: t('settings.accessibility.presets.screen-reader.description_mobile', 'No conversation grouping, so every message is a row of its own.'),
|
||||
};
|
||||
case 'low-vision':
|
||||
return {
|
||||
label: t('settings.accessibility.presets.low-vision.label', 'Low vision'),
|
||||
description: t('settings.accessibility.presets.low-vision.description_mobile', 'Text at 150% and labels under the toolbar icons.'),
|
||||
};
|
||||
case 'motion':
|
||||
return {
|
||||
label: t('settings.accessibility.presets.motion.label', 'Motion and seizure safe'),
|
||||
description: t('settings.accessibility.presets.motion.description_mobile', 'No animations or sliding transitions.'),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Settings > Accessibility (webmail components/settings/accessibility-settings.tsx):
|
||||
* the presets a phone can apply, text size, motion, and the timing and
|
||||
* confirmation options. Reduce motion also follows the system setting
|
||||
* (theme/dynamic useShouldAnimate).
|
||||
*/
|
||||
export function AccessibilitySettings() {
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const hydrated = useSettingsStore((s) => s.hydrated);
|
||||
const hydrate = useSettingsStore((s) => s.hydrate);
|
||||
const update = useSettingsStore((s) => s.updateSetting);
|
||||
const fontSize = useSettingsStore((s) => s.fontSize);
|
||||
const animationsEnabled = useSettingsStore((s) => s.animationsEnabled);
|
||||
const toastDuration = useSettingsStore((s) => s.toastDuration);
|
||||
const sendConfirmation = useSettingsStore((s) => s.sendConfirmation);
|
||||
const confirmDelete = useSettingsStore((s) => s.confirmDelete);
|
||||
const confirmArchive = useSettingsStore((s) => s.confirmArchive);
|
||||
|
||||
React.useEffect(() => {
|
||||
if (!hydrated) void hydrate();
|
||||
}, [hydrated, hydrate]);
|
||||
|
||||
const presetName = (id: AccessibilityPresetId) => presetCopy(id, t).label;
|
||||
|
||||
const applyPreset = (id: AccessibilityPresetId) => {
|
||||
const name = presetName(id);
|
||||
const changes = presetChanges(id, useSettingsStore.getState());
|
||||
if (changes.length === 0) {
|
||||
toast.info(t('settings.accessibility.presets.nothing_changed', '{name}: your settings already match.', { name }));
|
||||
return;
|
||||
}
|
||||
applyPresetChanges(changes, update);
|
||||
toast.success(
|
||||
t('settings.accessibility.presets.applied_mobile', '{name} changed {count, plural, one {# setting} other {# settings}}', {
|
||||
name,
|
||||
count: changes.length,
|
||||
}),
|
||||
{
|
||||
action: {
|
||||
label: t('settings.accessibility.presets.undo', 'Undo'),
|
||||
onPress: () => undoPresetChanges(changes, useSettingsStore.getState(), update),
|
||||
},
|
||||
},
|
||||
);
|
||||
};
|
||||
|
||||
const durationLabel = (value: number) => {
|
||||
if (value < 0) return t('settings.accessibility.toast_duration.automatic', 'Automatic (longer with a screen reader)');
|
||||
if (value === 0) return t('settings.accessibility.toast_duration.until_closed', 'Until I close them');
|
||||
return t('settings.accessibility.toast_duration.seconds', '{seconds} seconds', { seconds: value });
|
||||
};
|
||||
|
||||
return (
|
||||
<View style={{ gap: spacing.xxxl }}>
|
||||
<SettingsSection
|
||||
title={t('settings.accessibility.presets.title', 'Presets')}
|
||||
description={t('settings.accessibility.presets.description', 'Each preset changes several settings at once. Afterwards they are normal settings you can change one by one.')}
|
||||
>
|
||||
{ACCESSIBILITY_PRESET_IDS.map((id, i) => (
|
||||
<SettingItem
|
||||
key={id}
|
||||
label={presetName(id)}
|
||||
description={presetCopy(id, t).description}
|
||||
noBorder={i === ACCESSIBILITY_PRESET_IDS.length - 1}
|
||||
>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onPress={() => applyPreset(id)}
|
||||
accessibilityLabel={t('settings.accessibility.presets.apply_named', 'Apply {name}', { name: presetName(id) })}
|
||||
>
|
||||
{t('settings.accessibility.presets.apply', 'Apply')}
|
||||
</Button>
|
||||
</SettingItem>
|
||||
))}
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('settings.accessibility.sections.text', 'Text and reading')}>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.text_size.label', 'Text size')}
|
||||
description={t('settings.accessibility.text_size.description_mobile', "Scales the app's text on top of your phone's text size. 100% is your phone's size.")}
|
||||
noBorder
|
||||
>
|
||||
<Select
|
||||
value={fontSize}
|
||||
onChange={(v) => update('fontSize', v as FontSize)}
|
||||
options={FONT_SIZES.map((size) => ({ value: size, label: TEXT_SIZE_LABELS[size] }))}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('settings.accessibility.sections.motion', 'Motion')}>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.reduce_motion.label', 'Reduce motion')}
|
||||
description={t('settings.accessibility.reduce_motion.description_mobile', "Turns off sliding panels and transitions. The app also does this when reduced motion is on in your phone's settings.")}
|
||||
noBorder
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={!animationsEnabled}
|
||||
onChange={(reduce) => update('animationsEnabled', !reduce)}
|
||||
accessibilityLabel={t('settings.accessibility.reduce_motion.label', 'Reduce motion')}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
<SettingsSection title={t('settings.accessibility.sections.timing', 'Timing and mistakes')}>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.toast_duration.label', 'Notification duration')}
|
||||
description={t('settings.accessibility.toast_duration.description', 'How long messages such as "Moved to Archive" with their Undo button stay on screen.')}
|
||||
>
|
||||
<Select
|
||||
value={String(toastDuration)}
|
||||
onChange={(v) => update('toastDuration', Number(v))}
|
||||
options={TOAST_DURATION_CHOICES.map((value) => ({ value: String(value), label: durationLabel(value) }))}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.confirm_send.label', 'Confirm before sending')}
|
||||
description={t('settings.accessibility.confirm_send.description', 'Asks before a message is sent.')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={sendConfirmation}
|
||||
onChange={(v) => update('sendConfirmation', v)}
|
||||
accessibilityLabel={t('settings.accessibility.confirm_send.label', 'Confirm before sending')}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.confirm_delete.label', 'Confirm before deleting')}
|
||||
description={t('settings.accessibility.confirm_delete.description', 'Asks before a message is deleted, also for single messages.')}
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={confirmDelete}
|
||||
onChange={(v) => update('confirmDelete', v)}
|
||||
accessibilityLabel={t('settings.accessibility.confirm_delete.label', 'Confirm before deleting')}
|
||||
/>
|
||||
</SettingItem>
|
||||
<SettingItem
|
||||
label={t('settings.accessibility.confirm_archive.label', 'Confirm before archiving')}
|
||||
description={t('settings.accessibility.confirm_archive.description', 'Asks before a message is archived.')}
|
||||
noBorder
|
||||
>
|
||||
<ToggleSwitch
|
||||
checked={confirmArchive}
|
||||
onChange={(v) => update('confirmArchive', v)}
|
||||
accessibilityLabel={t('settings.accessibility.confirm_archive.label', 'Confirm before archiving')}
|
||||
/>
|
||||
</SettingItem>
|
||||
</SettingsSection>
|
||||
|
||||
</View>
|
||||
);
|
||||
}
|
||||
@@ -3,8 +3,9 @@ import { View, Text, StyleSheet } from 'react-native';
|
||||
import { SettingsSection, SettingItem, RadioGroup, ToggleSwitch } from './settings-section';
|
||||
import { spacing, radius, typography, type ThemePalette } from '../../theme/tokens';
|
||||
import { useColors } from '../../theme/colors';
|
||||
import { useSettingsStore, type ThemeMode, type FontSize, type Density } from '../../stores/settings-store';
|
||||
import { useSettingsStore, type ThemeMode, type Density } from '../../stores/settings-store';
|
||||
import { useLocaleStore } from '../../stores/locale-store';
|
||||
import { setPendingSettingsTab } from '../../navigation/pending-settings-tab';
|
||||
|
||||
type Theme = ThemeMode;
|
||||
|
||||
@@ -85,14 +86,10 @@ export function AppearanceSettings() {
|
||||
const theme = useSettingsStore((s) => s.theme);
|
||||
const t = useLocaleStore((s) => s.t);
|
||||
const setTheme = useSettingsStore((s) => s.setTheme);
|
||||
const fontSize = useSettingsStore((s) => s.fontSize);
|
||||
const setFontSize = useSettingsStore((s) => s.setFontSize);
|
||||
const density = useSettingsStore((s) => s.density);
|
||||
const setDensity = useSettingsStore((s) => s.setDensity);
|
||||
const showToolbarLabels = useSettingsStore((s) => s.showToolbarLabels);
|
||||
const setShowToolbarLabels = useSettingsStore((s) => s.setShowToolbarLabels);
|
||||
const animationsEnabled = useSettingsStore((s) => s.animationsEnabled);
|
||||
const setAnimationsEnabled = useSettingsStore((s) => s.setAnimationsEnabled);
|
||||
const senderFavicons = useSettingsStore((s) => s.senderFavicons);
|
||||
const setSenderFavicons = useSettingsStore((s) => s.setSenderFavicons);
|
||||
const hydrated = useSettingsStore((s) => s.hydrated);
|
||||
@@ -119,17 +116,13 @@ export function AppearanceSettings() {
|
||||
/>
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('settings.appearance.font_size.label', "Font Size")} description={t('settings.appearance.font_size.description', "Adjust text size for better readability")}>
|
||||
<RadioGroup
|
||||
value={fontSize}
|
||||
onChange={(v) => setFontSize(v as FontSize)}
|
||||
options={[
|
||||
{ value: 'small', label: t('settings.appearance.font_size.small', "Small") },
|
||||
{ value: 'medium', label: t('settings.appearance.font_size.medium', "Medium") },
|
||||
{ value: 'large', label: t('settings.appearance.font_size.large', "Large") },
|
||||
]}
|
||||
/>
|
||||
</SettingItem>
|
||||
{/* Text size and motion moved to Settings > Accessibility. */}
|
||||
<SettingItem
|
||||
label={t('settings.appearance.accessibility_link.label', 'Text size and motion')}
|
||||
description={t('settings.appearance.accessibility_link.description_mobile', 'Text size, reduced motion and the accessibility presets are on the Accessibility page.')}
|
||||
onPress={() => setPendingSettingsTab('accessibility')}
|
||||
link="page"
|
||||
/>
|
||||
|
||||
<View>
|
||||
<SettingItem label={t('settings.appearance.list_density.label', "Density")} description={t('settings.appearance.list_density.description', "Control spacing and padding across the UI")} noBorder />
|
||||
@@ -151,10 +144,6 @@ export function AppearanceSettings() {
|
||||
<ToggleSwitch checked={showToolbarLabels} onChange={setShowToolbarLabels} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem label={t('settings.appearance.animations.label', "Enable Animations")} description={t('settings.appearance.animations.description', "Show smooth transitions and effects")}>
|
||||
<ToggleSwitch checked={animationsEnabled} onChange={setAnimationsEnabled} />
|
||||
</SettingItem>
|
||||
|
||||
<SettingItem
|
||||
label={t('settings.advanced.sender_favicons.label', 'Sender Favicons')}
|
||||
description={t('settings.advanced.sender_favicons.description', 'Show website icons as profile pictures for business senders')}
|
||||
|
||||
Reference in New Issue
Block a user