mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-01 16:05:39 +00:00
fix: scale the reader's header and toolbar with the text size setting
This commit is contained in:
@@ -0,0 +1,36 @@
|
|||||||
|
import { describe, expect, it } from 'vitest';
|
||||||
|
import fs from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
// At 200% text size the reader's header and toolbar stayed at 100% while
|
||||||
|
// the list and the body grew: their styles were built from the fixed
|
||||||
|
// tokens. They now take the user's text size from useTypography, as the
|
||||||
|
// list does. No React Native renderer in the test env, so this checks the
|
||||||
|
// wiring.
|
||||||
|
|
||||||
|
const read = (rel: string) => fs.readFileSync(path.resolve(__dirname, '..', '..', rel), 'utf8');
|
||||||
|
|
||||||
|
const FILES = {
|
||||||
|
'screens/EmailThreadScreen.tsx': 6,
|
||||||
|
'components/email/MessageHeader.tsx': 1,
|
||||||
|
'components/email/ThreadMessageCard.tsx': 2,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('reader text size', () => {
|
||||||
|
for (const [file, uses] of Object.entries(FILES)) {
|
||||||
|
it(`builds the styles of ${file} at the user's text size`, () => {
|
||||||
|
const src = read(file);
|
||||||
|
expect(src).toMatch(/function makeStyles\(c: ThemePalette, typography: ScaledTypography = tokenTypography\)/);
|
||||||
|
expect(src).not.toMatch(/makeStyles\(c\)/);
|
||||||
|
expect((src.match(/const scaledType = useTypography\(\);\s+const styles = React\.useMemo\(\(\) => makeStyles\(c, scaledType\), \[c, scaledType\]\);/g) ?? []).length)
|
||||||
|
.toBe(uses);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
it('scales the subject line with the text size', () => {
|
||||||
|
const src = read('screens/EmailThreadScreen.tsx');
|
||||||
|
const subject = src.slice(src.indexOf(' subjectText: {'), src.indexOf(' subjectText: {') + 300);
|
||||||
|
expect(subject).toMatch(/fontSize: typography\.h3\.fontSize,/);
|
||||||
|
expect(subject).not.toMatch(/fontSize: 18,/);
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -4,7 +4,10 @@ import {
|
|||||||
Star, ChevronDown, ChevronUp, Reply, Forward, ShieldCheck, ShieldAlert, ShieldQuestion, Lock,
|
Star, ChevronDown, ChevronUp, Reply, Forward, ShieldCheck, ShieldAlert, ShieldQuestion, Lock,
|
||||||
} from '../icons';
|
} from '../icons';
|
||||||
import type { Email, EmailAddress, Identity } from '../../api/types';
|
import type { Email, EmailAddress, Identity } from '../../api/types';
|
||||||
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
import { spacing, radius, typography, typography as tokenTypography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
||||||
|
import { useTypography } from '../../theme/dynamic';
|
||||||
|
// The user's text size (Settings > Accessibility), as the list has it.
|
||||||
|
type ScaledTypography = ReturnType<typeof useTypography>;
|
||||||
import { useColors } from '../../theme/colors';
|
import { useColors } from '../../theme/colors';
|
||||||
import SenderAvatar from '../SenderAvatar';
|
import SenderAvatar from '../SenderAvatar';
|
||||||
import { useSettingsStore } from '../../stores/settings-store';
|
import { useSettingsStore } from '../../stores/settings-store';
|
||||||
@@ -99,7 +102,8 @@ function DetailRow({ label, value, styles, mono }: { label: string; value?: stri
|
|||||||
*/
|
*/
|
||||||
export function MessageHeader({ email, identities, headerInfo, onToggleStar, onAddressPress, compact, jmapAccountId }: Props) {
|
export function MessageHeader({ email, identities, headerInfo, onToggleStar, onAddressPress, compact, jmapAccountId }: Props) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
const locale = useLocaleStore((s) => s.locale);
|
const locale = useLocaleStore((s) => s.locale);
|
||||||
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
@@ -372,7 +376,7 @@ export function MessageHeader({ email, identities, headerInfo, onToggleStar, onA
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStyles(c: ThemePalette) {
|
function makeStyles(c: ThemePalette, typography: ScaledTypography = tokenTypography) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
block: {
|
block: {
|
||||||
backgroundColor: c.background,
|
backgroundColor: c.background,
|
||||||
@@ -430,7 +434,7 @@ function makeStyles(c: ThemePalette) {
|
|||||||
detailRow: { flexDirection: 'row', gap: spacing.sm },
|
detailRow: { flexDirection: 'row', gap: spacing.sm },
|
||||||
detailLabel: { ...typography.caption, color: c.textMuted, width: 92 },
|
detailLabel: { ...typography.caption, color: c.textMuted, width: 92 },
|
||||||
detailValue: { ...typography.caption, color: c.text, flex: 1 },
|
detailValue: { ...typography.caption, color: c.text, flex: 1 },
|
||||||
detailMono: { fontFamily: 'monospace', fontSize: 11 },
|
detailMono: { fontFamily: 'monospace', fontSize: Math.round((11 * typography.caption.fontSize) / 12) },
|
||||||
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.xs, marginBottom: spacing.xs },
|
chips: { flexDirection: 'row', flexWrap: 'wrap', gap: spacing.xs, marginBottom: spacing.xs },
|
||||||
chip: {
|
chip: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -2,7 +2,10 @@ import React from 'react';
|
|||||||
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
import { View, Text, StyleSheet, Pressable } from 'react-native';
|
||||||
import { Paperclip, Reply, ReplyAll, Forward, Star } from '../icons';
|
import { Paperclip, Reply, ReplyAll, Forward, Star } from '../icons';
|
||||||
import type { Email } from '../../api/types';
|
import type { Email } from '../../api/types';
|
||||||
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
import { spacing, radius, typography, typography as tokenTypography, componentSizes, type ThemePalette } from '../../theme/tokens';
|
||||||
|
import { useTypography } from '../../theme/dynamic';
|
||||||
|
// The user's text size (Settings > Accessibility), as the list has it.
|
||||||
|
type ScaledTypography = ReturnType<typeof useTypography>;
|
||||||
import { useColors } from '../../theme/colors';
|
import { useColors } from '../../theme/colors';
|
||||||
import { useSettingsStore } from '../../stores/settings-store';
|
import { useSettingsStore } from '../../stores/settings-store';
|
||||||
import { useLocaleStore } from '../../stores/locale-store';
|
import { useLocaleStore } from '../../stores/locale-store';
|
||||||
@@ -42,7 +45,8 @@ export function ThreadMessageCard({
|
|||||||
}: Props) {
|
}: Props) {
|
||||||
const { email, onToggleStar } = content;
|
const { email, onToggleStar } = content;
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
const locale = useLocaleStore((s) => s.locale);
|
const locale = useLocaleStore((s) => s.locale);
|
||||||
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
const timeFormat = useSettingsStore((s) => s.timeFormat);
|
||||||
@@ -113,7 +117,8 @@ export function ThreadMessageCard({
|
|||||||
*/
|
*/
|
||||||
export function ThreadCardPlaceholder() {
|
export function ThreadCardPlaceholder() {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
return (
|
return (
|
||||||
<View style={styles.collapsed}>
|
<View style={styles.collapsed}>
|
||||||
<View style={styles.placeholderAvatar} />
|
<View style={styles.placeholderAvatar} />
|
||||||
@@ -125,7 +130,7 @@ export function ThreadCardPlaceholder() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStyles(c: ThemePalette) {
|
function makeStyles(c: ThemePalette, typography: ScaledTypography = tokenTypography) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
collapsed: {
|
collapsed: {
|
||||||
flexDirection: 'row',
|
flexDirection: 'row',
|
||||||
|
|||||||
@@ -20,7 +20,10 @@ import { cardReplyActions } from '../lib/thread-card-actions';
|
|||||||
import { NewTagRow } from '../components/NewTagRow';
|
import { NewTagRow } from '../components/NewTagRow';
|
||||||
import { QuickRuleSheet } from '../components/filters/QuickRuleSheet';
|
import { QuickRuleSheet } from '../components/filters/QuickRuleSheet';
|
||||||
import { isSieveSupported } from '../api/sieve';
|
import { isSieveSupported } from '../api/sieve';
|
||||||
import { spacing, radius, typography, componentSizes, type ThemePalette } from '../theme/tokens';
|
import { spacing, radius, typography, typography as tokenTypography, componentSizes, type ThemePalette } from '../theme/tokens';
|
||||||
|
import { useTypography } from '../theme/dynamic';
|
||||||
|
// The user's text size (Settings > Accessibility), as the list has it.
|
||||||
|
type ScaledTypography = ReturnType<typeof useTypography>;
|
||||||
import { useColors, useResolvedTheme } from '../theme/colors';
|
import { useColors, useResolvedTheme } from '../theme/colors';
|
||||||
import { MoveSheet } from '../components/MoveSheet';
|
import { MoveSheet } from '../components/MoveSheet';
|
||||||
import { ThreadMessageCard, ThreadCardPlaceholder } from '../components/email/ThreadMessageCard';
|
import { ThreadMessageCard, ThreadCardPlaceholder } from '../components/email/ThreadMessageCard';
|
||||||
@@ -97,7 +100,8 @@ export default function EmailThreadScreen(props: Props) {
|
|||||||
|
|
||||||
function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Props) {
|
function EmailViewer({ route, navigation, embedded = false, onActiveChange }: Props) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
const { t } = useLocaleStore();
|
const { t } = useLocaleStore();
|
||||||
// The JMAP account the message lives in, as the screen that opened it named
|
// The JMAP account the message lives in, as the screen that opened it named
|
||||||
// it (undefined = the user's own). Never the open folder's: that may be
|
// it (undefined = the user's own). Never the open folder's: that may be
|
||||||
@@ -1398,7 +1402,8 @@ function MoreMenuSheet({
|
|||||||
onForwardAsAttachment, onRules, onViewSource, onExport,
|
onForwardAsAttachment, onRules, onViewSource, onExport,
|
||||||
}: MoreMenuSheetProps) {
|
}: MoreMenuSheetProps) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const slideY = React.useRef(new Animated.Value(600)).current;
|
const slideY = React.useRef(new Animated.Value(600)).current;
|
||||||
@@ -1564,7 +1569,8 @@ interface TagMenuSheetProps {
|
|||||||
|
|
||||||
function TagMenuSheet({ visible, onClose, keywords, activeKeywords, onToggle }: TagMenuSheetProps) {
|
function TagMenuSheet({ visible, onClose, keywords, activeKeywords, onToggle }: TagMenuSheetProps) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
const t = useLocaleStore((s) => s.t);
|
const t = useLocaleStore((s) => s.t);
|
||||||
const insets = useSafeAreaInsets();
|
const insets = useSafeAreaInsets();
|
||||||
const slideY = React.useRef(new Animated.Value(500)).current;
|
const slideY = React.useRef(new Animated.Value(500)).current;
|
||||||
@@ -1649,7 +1655,8 @@ function MoreMenuItem({
|
|||||||
icon, label, onPress, trailing,
|
icon, label, onPress, trailing,
|
||||||
}: { icon: React.ReactNode; label: string; onPress?: () => void; trailing?: React.ReactNode }) {
|
}: { icon: React.ReactNode; label: string; onPress?: () => void; trailing?: React.ReactNode }) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
onPress={onPress}
|
onPress={onPress}
|
||||||
@@ -1666,7 +1673,8 @@ function ToolbarButton({
|
|||||||
icon, label, onPress,
|
icon, label, onPress,
|
||||||
}: { icon: React.ReactNode; label: string; onPress?: () => void }) {
|
}: { icon: React.ReactNode; label: string; onPress?: () => void }) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
// Icons only in a short (landscape) window, which leaves room for the message.
|
// Icons only in a short (landscape) window, which leaves room for the message.
|
||||||
const labelsSetting = useSettingsStore((s) => s.showToolbarLabels);
|
const labelsSetting = useSettingsStore((s) => s.showToolbarLabels);
|
||||||
const { shortHeight } = useLayout();
|
const { shortHeight } = useLayout();
|
||||||
@@ -1688,7 +1696,8 @@ function BottomBarButton({
|
|||||||
icon, label, onPress, disabled,
|
icon, label, onPress, disabled,
|
||||||
}: { icon: React.ReactNode; label: string; onPress?: () => void; disabled?: boolean }) {
|
}: { icon: React.ReactNode; label: string; onPress?: () => void; disabled?: boolean }) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const scaledType = useTypography();
|
||||||
|
const styles = React.useMemo(() => makeStyles(c, scaledType), [c, scaledType]);
|
||||||
// Icons only in a short (landscape) window, which leaves room for the message.
|
// Icons only in a short (landscape) window, which leaves room for the message.
|
||||||
const labelsSetting = useSettingsStore((s) => s.showToolbarLabels);
|
const labelsSetting = useSettingsStore((s) => s.showToolbarLabels);
|
||||||
const { shortHeight } = useLayout();
|
const { shortHeight } = useLayout();
|
||||||
@@ -1710,7 +1719,7 @@ function BottomBarButton({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function makeStyles(c: ThemePalette) {
|
function makeStyles(c: ThemePalette, typography: ScaledTypography = tokenTypography) {
|
||||||
return StyleSheet.create({
|
return StyleSheet.create({
|
||||||
container: { flex: 1, backgroundColor: c.background },
|
container: { flex: 1, backgroundColor: c.background },
|
||||||
|
|
||||||
@@ -1786,9 +1795,10 @@ function makeStyles(c: ThemePalette) {
|
|||||||
// Webmail viewer on a phone: text-lg font-bold tracking-tight.
|
// Webmail viewer on a phone: text-lg font-bold tracking-tight.
|
||||||
subjectText: {
|
subjectText: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: 18,
|
// 18 / 26 at the default text size, scaled with it.
|
||||||
|
fontSize: typography.h3.fontSize,
|
||||||
fontWeight: '700',
|
fontWeight: '700',
|
||||||
lineHeight: 26,
|
lineHeight: Math.round((26 * typography.h3.fontSize) / 18),
|
||||||
color: c.text,
|
color: c.text,
|
||||||
letterSpacing: -0.2,
|
letterSpacing: -0.2,
|
||||||
},
|
},
|
||||||
|
|||||||
Reference in New Issue
Block a user