Merge branch 'cu-readx' into catch-up-2026-11

This commit is contained in:
Linus Rath
2026-10-31 14:59:37 +01:00
41 changed files with 3880 additions and 219 deletions
@@ -1,10 +1,11 @@
import React from 'react';
import { View } from 'react-native';
import { Platform, 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 { APP_FONTS, FONT_SIZES, useSettingsStore, type AppFont, type FontSize } from '../../stores/settings-store';
import { uiFontsBundled } from '../../theme/reading-fonts';
import { toast } from '../../stores/toast-store';
import {
ACCESSIBILITY_PRESET_IDS,
@@ -15,6 +16,14 @@ import {
} from '../../lib/accessibility-presets';
import { TOAST_DURATION_CHOICES } from '../../lib/toast-duration';
// Font names are names: the same in every language (webmail
// accessibility-settings.tsx).
const FONT_NAMES: Record<Exclude<AppFont, 'default'>, string> = {
atkinson: 'Atkinson Hyperlegible',
lexend: 'Lexend',
opendyslexic: 'OpenDyslexic',
};
// Percentages read the same in every language the app has.
const TEXT_SIZE_LABELS: Record<FontSize, string> = {
small: '87.5%',
@@ -41,6 +50,11 @@ function presetCopy(id: AccessibilityPresetId, t: Translate): { label: string; d
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 'dyslexia':
return {
label: t('settings.accessibility.presets.dyslexia.label', 'Dyslexia'),
description: t('settings.accessibility.presets.dyslexia.description_mobile', 'Atkinson Hyperlegible as the font, also for plain-text messages.'),
};
case 'motion':
return {
label: t('settings.accessibility.presets.motion.label', 'Motion and seizure safe'),
@@ -61,6 +75,9 @@ export function AccessibilitySettings() {
const hydrate = useSettingsStore((s) => s.hydrate);
const update = useSettingsStore((s) => s.updateSetting);
const fontSize = useSettingsStore((s) => s.fontSize);
const appFont = useSettingsStore((s) => s.appFont);
const colorBlindPalette = useSettingsStore((s) => s.colorBlindPalette);
const plainLanguage = useSettingsStore((s) => s.plainLanguage);
const animationsEnabled = useSettingsStore((s) => s.animationsEnabled);
const toastDuration = useSettingsStore((s) => s.toastDuration);
const sendConfirmation = useSettingsStore((s) => s.sendConfirmation);
@@ -130,7 +147,6 @@ export function AccessibilitySettings() {
<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}
@@ -138,6 +154,46 @@ export function AccessibilitySettings() {
options={FONT_SIZES.map((size) => ({ value: size, label: TEXT_SIZE_LABELS[size] }))}
/>
</SettingItem>
<SettingItem
label={t('settings.accessibility.font.label', 'Font')}
description={uiFontsBundled(Platform.OS)
? t('settings.accessibility.font.description', 'Atkinson Hyperlegible and Lexend are designed to be easy to tell apart; OpenDyslexic helps some people with dyslexia. They cover Latin script.')
: t('settings.accessibility.font.description_ios', 'Atkinson Hyperlegible and Lexend are designed to be easy to tell apart; OpenDyslexic helps some people with dyslexia. On this device the font is used for plain-text messages.')}
>
<Select
value={appFont}
onChange={(v) => update('appFont', v as AppFont)}
options={APP_FONTS.map((font) => ({
value: font,
label: font === 'default' ? t('settings.accessibility.font.default', 'Theme font') : FONT_NAMES[font],
}))}
/>
</SettingItem>
<SettingItem
label={t('settings.accessibility.plain_language.label', 'Plain language')}
description={t('settings.accessibility.plain_language.description_mobile', 'Simpler wording for settings and error messages. Available in English and German.')}
noBorder
>
<ToggleSwitch
checked={plainLanguage}
onChange={(v) => update('plainLanguage', v)}
accessibilityLabel={t('settings.accessibility.plain_language.label', 'Plain language')}
/>
</SettingItem>
</SettingsSection>
<SettingsSection title={t('settings.accessibility.sections.contrast', 'Colour and contrast')}>
<SettingItem
label={t('settings.accessibility.cvd_palette.label', 'Colour-blind-safe colours')}
description={t('settings.accessibility.cvd_palette.description_mobile', 'Uses tag and calendar colours that stay distinct with red-green or blue-yellow colour blindness.')}
noBorder
>
<ToggleSwitch
checked={colorBlindPalette}
onChange={(v) => update('colorBlindPalette', v)}
accessibilityLabel={t('settings.accessibility.cvd_palette.label', 'Colour-blind-safe colours')}
/>
</SettingItem>
</SettingsSection>
<SettingsSection title={t('settings.accessibility.sections.motion', 'Motion')}>
+6 -4
View File
@@ -113,7 +113,7 @@ export function KeywordSettings() {
/>
);
}
const palette = c.tags[kw.color];
const palette = (c.tagsAsPicked ?? c.tags)[kw.color];
return (
<View key={kw.id} style={styles.kwRow}>
<View style={[styles.kwDot, { backgroundColor: palette.dot }]} />
@@ -178,7 +178,7 @@ export function KeywordSettings() {
</Text>
{scan.found.map((kw) => (
<View key={kw.id} style={styles.kwRow}>
<View style={[styles.kwDot, { backgroundColor: c.tags[kw.color]?.dot ?? c.textMuted }]} />
<View style={[styles.kwDot, { backgroundColor: (c.tagsAsPicked ?? c.tags)[kw.color]?.dot ?? c.textMuted }]} />
<View style={{ flex: 1 }}>
<Text style={styles.kwLabel}>{kw.label}</Text>
<Text style={styles.kwId}>{kw.keyword} · {kw.count}</Text>
@@ -251,7 +251,7 @@ function SharedTagsSection() {
) : (
tags.map((kw) => (
<View key={kw.id} style={styles.kwRow}>
<View style={[styles.kwDot, { backgroundColor: c.tags[kw.color]?.dot ?? c.textMuted }]} />
<View style={[styles.kwDot, { backgroundColor: (c.tagsAsPicked ?? c.tags)[kw.color]?.dot ?? c.textMuted }]} />
<Text style={styles.kwLabel}>{kw.label}</Text>
<Text style={styles.kwId}>$label:{kw.id}</Text>
</View>
@@ -313,7 +313,9 @@ function KeywordForm({ initial, existingIds, onSave, onCancel }: KeywordFormProp
<Text style={styles.formLabel}>{t('settings.keywords.color_field', "Color")}</Text>
<View style={styles.palette}>
{PALETTE_KEYS.map((key) => {
const p = c.tags[key];
// The colours as picked, also with colour-blind-safe colours on
// (webmail keyword-settings.tsx draws KEYWORD_PALETTE).
const p = (c.tagsAsPicked ?? c.tags)[key];
return (
<Pressable
key={key}