mirror of
https://github.com/bulwarkmail/native.git
synced 2026-11-04 00:06:53 +00:00
fix: translate the tab bar and label the unified inbox and drawer buttons
The bottom tabs showed their route names ("Mail", "Calendar", …) in
every language; they now use the webmail's navigation names, and an
unavailable tab says so with one translated message instead of an
English "(unavailable)" suffix. The first loading screen is
translated as well.
The Unified Inbox selection toolbar, its back and clear-search buttons,
and the drawer's close, expand/collapse and remove-account buttons get
screen reader labels. Its unknown-sender fallback, selection count and
"account(s) could not be loaded" banner are proper translations and
plurals now.
The key harvest and the translation coverage test did not look at
App.tsx, which sits next to src/; they do now.
This commit is contained in:
@@ -200,6 +200,16 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
|||||||
const hasContacts = useHasContacts();
|
const hasContacts = useHasContacts();
|
||||||
const hasFiles = useHasFiles();
|
const hasFiles = useHasFiles();
|
||||||
const disabledTabStyle = { opacity: 0.4 } as const;
|
const disabledTabStyle = { opacity: 0.4 } as const;
|
||||||
|
const t = useLocaleStore((state) => state.t);
|
||||||
|
// The webmail's navigation names; a tab the server lacks says so.
|
||||||
|
const tabLabels = {
|
||||||
|
mail: t('sidebar.mail', 'Mail'),
|
||||||
|
calendar: t('sidebar.calendar', 'Calendar'),
|
||||||
|
contacts: t('sidebar.contacts', 'Contacts'),
|
||||||
|
files: t('sidebar.files', 'Files'),
|
||||||
|
settings: t('sidebar.settings', 'Settings'),
|
||||||
|
};
|
||||||
|
const unavailable = (name: string) => t('sidebar.tab_unavailable', '{name} (unavailable)', { name });
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={{ flex: 1, backgroundColor: c.background }}>
|
<View style={{ flex: 1, backgroundColor: c.background }}>
|
||||||
@@ -231,6 +241,7 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
|||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Mail"
|
name="Mail"
|
||||||
options={{
|
options={{
|
||||||
|
title: tabLabels.mail,
|
||||||
tabBarIcon: ({ color, size }) => <Mail size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <Mail size={size} color={color} />,
|
||||||
tabBarBadge: inboxUnreadCount > 0 ? (inboxUnreadCount > 99 ? '99+' : inboxUnreadCount) : undefined,
|
tabBarBadge: inboxUnreadCount > 0 ? (inboxUnreadCount > 99 ? '99+' : inboxUnreadCount) : undefined,
|
||||||
tabBarBadgeStyle: {
|
tabBarBadgeStyle: {
|
||||||
@@ -276,9 +287,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
|||||||
name="Calendar"
|
name="Calendar"
|
||||||
component={CalendarScreen}
|
component={CalendarScreen}
|
||||||
options={{
|
options={{
|
||||||
|
title: tabLabels.calendar,
|
||||||
tabBarIcon: ({ color, size }) => <Calendar size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <Calendar size={size} color={color} />,
|
||||||
tabBarItemStyle: hasCalendar ? undefined : disabledTabStyle,
|
tabBarItemStyle: hasCalendar ? undefined : disabledTabStyle,
|
||||||
tabBarAccessibilityLabel: hasCalendar ? 'Calendar' : 'Calendar (unavailable)',
|
tabBarAccessibilityLabel: hasCalendar ? tabLabels.calendar : unavailable(tabLabels.calendar),
|
||||||
}}
|
}}
|
||||||
listeners={{
|
listeners={{
|
||||||
tabPress: (e) => {
|
tabPress: (e) => {
|
||||||
@@ -290,9 +302,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
|||||||
name="Contacts"
|
name="Contacts"
|
||||||
component={ContactsScreen}
|
component={ContactsScreen}
|
||||||
options={{
|
options={{
|
||||||
|
title: tabLabels.contacts,
|
||||||
tabBarIcon: ({ color, size }) => <BookUser size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <BookUser size={size} color={color} />,
|
||||||
tabBarItemStyle: hasContacts ? undefined : disabledTabStyle,
|
tabBarItemStyle: hasContacts ? undefined : disabledTabStyle,
|
||||||
tabBarAccessibilityLabel: hasContacts ? 'Contacts' : 'Contacts (unavailable)',
|
tabBarAccessibilityLabel: hasContacts ? tabLabels.contacts : unavailable(tabLabels.contacts),
|
||||||
}}
|
}}
|
||||||
listeners={{
|
listeners={{
|
||||||
tabPress: (e) => {
|
tabPress: (e) => {
|
||||||
@@ -304,9 +317,10 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
|||||||
name="Files"
|
name="Files"
|
||||||
component={FilesScreen}
|
component={FilesScreen}
|
||||||
options={{
|
options={{
|
||||||
|
title: tabLabels.files,
|
||||||
tabBarIcon: ({ color, size }) => <HardDrive size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <HardDrive size={size} color={color} />,
|
||||||
tabBarItemStyle: hasFiles ? undefined : disabledTabStyle,
|
tabBarItemStyle: hasFiles ? undefined : disabledTabStyle,
|
||||||
tabBarAccessibilityLabel: hasFiles ? 'Files' : 'Files (unavailable)',
|
tabBarAccessibilityLabel: hasFiles ? tabLabels.files : unavailable(tabLabels.files),
|
||||||
}}
|
}}
|
||||||
listeners={{
|
listeners={{
|
||||||
tabPress: (e) => {
|
tabPress: (e) => {
|
||||||
@@ -317,6 +331,7 @@ function MainTabsNavigator({ navigation }: NativeStackScreenProps<RootStackParam
|
|||||||
<Tab.Screen
|
<Tab.Screen
|
||||||
name="Settings"
|
name="Settings"
|
||||||
options={{
|
options={{
|
||||||
|
title: tabLabels.settings,
|
||||||
tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />,
|
tabBarIcon: ({ color, size }) => <Settings size={size} color={color} />,
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -332,6 +347,7 @@ export default function App() {
|
|||||||
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
const isAuthenticated = useAuthStore((state) => state.isAuthenticated);
|
||||||
const client = useAuthStore((state) => state.client);
|
const client = useAuthStore((state) => state.client);
|
||||||
const restoreSession = useAuthStore((state) => state.restoreSession);
|
const restoreSession = useAuthStore((state) => state.restoreSession);
|
||||||
|
const t = useLocaleStore((state) => state.t);
|
||||||
|
|
||||||
// Resolve the user's theme preference to a concrete light/dark style for the
|
// Resolve the user's theme preference to a concrete light/dark style for the
|
||||||
// system status bar. The rest of the app's colors are still hard-coded dark
|
// system status bar. The rest of the app's colors are still hard-coded dark
|
||||||
@@ -700,7 +716,7 @@ export default function App() {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StatusBar style={statusBarStyle} />
|
<StatusBar style={statusBarStyle} />
|
||||||
<LoadingScreen message="Loading..." />
|
<LoadingScreen message={t('common.loading', 'Loading...')} />
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-2
@@ -988,10 +988,11 @@
|
|||||||
"remove_account_hint": "Long-press to remove this account",
|
"remove_account_hint": "Long-press to remove this account",
|
||||||
"remove_account_message": "Sign out of {account} on this device. Your mail stays on the server.",
|
"remove_account_message": "Sign out of {account} on this device. Your mail stays on the server.",
|
||||||
"remove_account_title": "Remove account?",
|
"remove_account_title": "Remove account?",
|
||||||
"show_unread": "Show unread"
|
"show_unread": "Show unread",
|
||||||
|
"tab_unavailable": "{name} (unavailable)"
|
||||||
},
|
},
|
||||||
"unified_mailbox": {
|
"unified_mailbox": {
|
||||||
"accounts_failed": "{count} account(s) could not be loaded"
|
"accounts_failed": "{count, plural, one {# account could not be loaded} other {# accounts could not be loaded}}"
|
||||||
},
|
},
|
||||||
"updates": {
|
"updates": {
|
||||||
"advisory": "Security advisory",
|
"advisory": "Security advisory",
|
||||||
|
|||||||
@@ -68,7 +68,8 @@ const KEY_ONLY_RE = /\btr?\(\s*'([^']+)'/g;
|
|||||||
|
|
||||||
const added = [];
|
const added = [];
|
||||||
const missingNoFallback = [];
|
const missingNoFallback = [];
|
||||||
for (const file of walk(SRC)) {
|
// App.tsx (the tab bar and loading screen) sits next to src/.
|
||||||
|
for (const file of [...walk(SRC), join(ROOT, 'App.tsx')]) {
|
||||||
const text = readFileSync(file, 'utf8');
|
const text = readFileSync(file, 'utf8');
|
||||||
const withFallback = new Map();
|
const withFallback = new Map();
|
||||||
for (const m of text.matchAll(CALL_RE)) {
|
for (const m of text.matchAll(CALL_RE)) {
|
||||||
|
|||||||
@@ -166,6 +166,7 @@ function SidebarRow({
|
|||||||
}: SidebarRowProps) {
|
}: SidebarRowProps) {
|
||||||
const c = useColors();
|
const c = useColors();
|
||||||
const styles = React.useMemo(() => makeStyles(c), [c]);
|
const styles = React.useMemo(() => makeStyles(c), [c]);
|
||||||
|
const t = useLocaleStore((s) => s.t);
|
||||||
const leftPad = ROW_PX_BASE + depth * INDENT_STEP;
|
const leftPad = ROW_PX_BASE + depth * INDENT_STEP;
|
||||||
return (
|
return (
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -184,6 +185,9 @@ function SidebarRow({
|
|||||||
onPress={onToggleExpand}
|
onPress={onToggleExpand}
|
||||||
hitSlop={6}
|
hitSlop={6}
|
||||||
style={styles.chevron}
|
style={styles.chevron}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={isExpanded ? t('sidebar.collapse', 'Collapse') : t('sidebar.expand', 'Expand')}
|
||||||
|
accessibilityState={{ expanded: isExpanded }}
|
||||||
>
|
>
|
||||||
{isExpanded ? (
|
{isExpanded ? (
|
||||||
<ChevronDown size={12} color={c.textMuted} />
|
<ChevronDown size={12} color={c.textMuted} />
|
||||||
@@ -758,7 +762,13 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
|||||||
<SafeAreaView style={styles.drawerSafe} edges={['top', 'bottom', 'left']}>
|
<SafeAreaView style={styles.drawerSafe} edges={['top', 'bottom', 'left']}>
|
||||||
{/* Header: close + account switcher */}
|
{/* Header: close + account switcher */}
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable onPress={onClose} style={styles.headerClose} hitSlop={8}>
|
<Pressable
|
||||||
|
onPress={onClose}
|
||||||
|
style={styles.headerClose}
|
||||||
|
hitSlop={8}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('common.close', 'Close')}
|
||||||
|
>
|
||||||
<X size={20} color={c.text} />
|
<X size={20} color={c.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable
|
<Pressable
|
||||||
@@ -860,7 +870,13 @@ export default function SidebarDrawer({ visible, onClose }: SidebarDrawerProps)
|
|||||||
</View>
|
</View>
|
||||||
</View>
|
</View>
|
||||||
{!isActive && (
|
{!isActive && (
|
||||||
<Pressable onPress={() => confirmRemoveAccount(acc)} hitSlop={8} style={styles.accountMenuRemove}>
|
<Pressable
|
||||||
|
onPress={() => confirmRemoveAccount(acc)}
|
||||||
|
hitSlop={8}
|
||||||
|
style={styles.accountMenuRemove}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('sidebar.remove_account', 'Remove account')}
|
||||||
|
>
|
||||||
<UserMinus size={14} color={c.textMuted} />
|
<UserMinus size={14} color={c.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -55,6 +55,10 @@ const TRANSLATED_FILES: Record<string, string[]> = {
|
|||||||
'components/email/QuickReplyBox.tsx': [],
|
'components/email/QuickReplyBox.tsx': [],
|
||||||
'components/email/AttachmentChips.tsx': [],
|
'components/email/AttachmentChips.tsx': [],
|
||||||
'components/email/AttachmentPreviewModal.tsx': [],
|
'components/email/AttachmentPreviewModal.tsx': [],
|
||||||
|
'screens/UnifiedInboxScreen.tsx': [],
|
||||||
|
'components/SidebarDrawer.tsx': [],
|
||||||
|
// The app shell (tab bar, loading screen), next to src/.
|
||||||
|
'../App.tsx': [],
|
||||||
};
|
};
|
||||||
|
|
||||||
describe('translated screens', () => {
|
describe('translated screens', () => {
|
||||||
|
|||||||
@@ -21,6 +21,11 @@ function flatten(obj: Record<string, unknown>, prefix = '', out = new Set<string
|
|||||||
return out;
|
return out;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// src/ plus App.tsx next to it (the tab bar and loading screen).
|
||||||
|
function sourceFiles(): string[] {
|
||||||
|
return [...walk(SRC_ROOT), join(SRC_ROOT, '..', 'App.tsx')];
|
||||||
|
}
|
||||||
|
|
||||||
function walk(dir: string, out: string[] = []): string[] {
|
function walk(dir: string, out: string[] = []): string[] {
|
||||||
for (const entry of readdirSync(dir)) {
|
for (const entry of readdirSync(dir)) {
|
||||||
const p = join(dir, entry);
|
const p = join(dir, entry);
|
||||||
@@ -48,7 +53,7 @@ describe('translation coverage', () => {
|
|||||||
|
|
||||||
it('every literal t() key in src exists in the English catalog', () => {
|
it('every literal t() key in src exists in the English catalog', () => {
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
for (const file of walk(SRC_ROOT)) {
|
for (const file of sourceFiles()) {
|
||||||
const text = readFileSync(file, 'utf8');
|
const text = readFileSync(file, 'utf8');
|
||||||
for (const m of text.matchAll(LITERAL_KEY_RE)) {
|
for (const m of text.matchAll(LITERAL_KEY_RE)) {
|
||||||
if (!en.has(m[1])) missing.push(`${m[1]} (${relative(SRC_ROOT, file)})`);
|
if (!en.has(m[1])) missing.push(`${m[1]} (${relative(SRC_ROOT, file)})`);
|
||||||
@@ -59,7 +64,7 @@ describe('translation coverage', () => {
|
|||||||
|
|
||||||
it('every template t() key prefix has at least one English entry', () => {
|
it('every template t() key prefix has at least one English entry', () => {
|
||||||
const missing: string[] = [];
|
const missing: string[] = [];
|
||||||
for (const file of walk(SRC_ROOT)) {
|
for (const file of sourceFiles()) {
|
||||||
const text = readFileSync(file, 'utf8');
|
const text = readFileSync(file, 'utf8');
|
||||||
for (const m of text.matchAll(TEMPLATE_KEY_RE)) {
|
for (const m of text.matchAll(TEMPLATE_KEY_RE)) {
|
||||||
const prefix = m[1];
|
const prefix = m[1];
|
||||||
|
|||||||
@@ -1366,7 +1366,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL
|
|||||||
{unreachedAccounts.length > 0 && (
|
{unreachedAccounts.length > 0 && (
|
||||||
<View style={[styles.emptyFolderBanner, { borderColor: c.error }]}>
|
<View style={[styles.emptyFolderBanner, { borderColor: c.error }]}>
|
||||||
<Text style={styles.emptyFolderHint} numberOfLines={2}>
|
<Text style={styles.emptyFolderHint} numberOfLines={2}>
|
||||||
{t('unified_mailbox.accounts_failed', `${unreachedAccounts.length} account(s) could not be loaded`, { count: unreachedAccounts.length })}
|
{t('unified_mailbox.accounts_failed', '{count, plural, one {# account could not be loaded} other {# accounts could not be loaded}}', { count: unreachedAccounts.length })}
|
||||||
{`: ${unreachedAccounts[0]}`}
|
{`: ${unreachedAccounts[0]}`}
|
||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ type Props = NativeStackScreenProps<RootStackParamList, 'UnifiedInbox'>;
|
|||||||
|
|
||||||
const PAGE_SIZE = 25;
|
const PAGE_SIZE = 25;
|
||||||
|
|
||||||
function senderName(email: UnifiedEmail, showRecipient: boolean): string {
|
function senderName(email: UnifiedEmail, showRecipient: boolean, unknown: string): string {
|
||||||
if (showRecipient) {
|
if (showRecipient) {
|
||||||
const to = email.to?.[0];
|
const to = email.to?.[0];
|
||||||
if (to) return to.name || to.email;
|
if (to) return to.name || to.email;
|
||||||
}
|
}
|
||||||
return email.from?.[0]?.name || email.from?.[0]?.email || 'Unknown';
|
return email.from?.[0]?.name || email.from?.[0]?.email || unknown;
|
||||||
}
|
}
|
||||||
|
|
||||||
function rowKey(e: UnifiedEmail): string {
|
function rowKey(e: UnifiedEmail): string {
|
||||||
@@ -330,7 +330,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
|||||||
)}
|
)}
|
||||||
<View style={styles.avatarWrap}>
|
<View style={styles.avatarWrap}>
|
||||||
<SenderAvatar
|
<SenderAvatar
|
||||||
name={senderName(item, showRecipient)}
|
name={senderName(item, showRecipient, t('email_viewer.unknown_sender', 'Unknown'))}
|
||||||
email={showRecipient ? item.to?.[0]?.email : item.from?.[0]?.email}
|
email={showRecipient ? item.to?.[0]?.email : item.from?.[0]?.email}
|
||||||
size={componentSizes.avatarMd}
|
size={componentSizes.avatarMd}
|
||||||
disableImages={role === 'junk' && !showAvatarsInJunk}
|
disableImages={role === 'junk' && !showAvatarsInJunk}
|
||||||
@@ -342,7 +342,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
|||||||
<View style={styles.content}>
|
<View style={styles.content}>
|
||||||
<View style={styles.line}>
|
<View style={styles.line}>
|
||||||
<Text style={[styles.sender, unread && styles.bold]} numberOfLines={1}>
|
<Text style={[styles.sender, unread && styles.bold]} numberOfLines={1}>
|
||||||
{senderName(item, showRecipient)}
|
{senderName(item, showRecipient, t('email_viewer.unknown_sender', 'Unknown'))}
|
||||||
</Text>
|
</Text>
|
||||||
{starred && <Star size={12} color={c.starred} fill={c.starred} />}
|
{starred && <Star size={12} color={c.starred} fill={c.starred} />}
|
||||||
{item.hasAttachment && <Paperclip size={12} color={c.textMuted} />}
|
{item.hasAttachment && <Paperclip size={12} color={c.textMuted} />}
|
||||||
@@ -384,35 +384,79 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
|||||||
<SafeAreaView style={styles.container} edges={['top']}>
|
<SafeAreaView style={styles.container} edges={['top']}>
|
||||||
{selectionMode ? (
|
{selectionMode ? (
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable onPress={clearSelection} style={styles.headerBtn} hitSlop={8}>
|
<Pressable
|
||||||
|
onPress={clearSelection}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
hitSlop={8}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('email_list.batch_actions.clear_selection', 'Clear selection')}
|
||||||
|
>
|
||||||
<X size={20} color={c.text} />
|
<X size={20} color={c.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={styles.headerTitle} numberOfLines={1}>
|
<Text style={styles.headerTitle} numberOfLines={1}>
|
||||||
{t('context_menu.items_selected', `${selected.size} selected`, { count: selected.size })}
|
{t('email_list.batch_actions.selected_messages', '{count, plural, one {1 email} other {# emails}} selected', { count: selected.size })}
|
||||||
</Text>
|
</Text>
|
||||||
<Pressable onPress={() => { setStarred(selectedEmails, !allStarred); clearSelection(); }} style={styles.headerBtn} hitSlop={6}>
|
<Pressable
|
||||||
|
onPress={() => { setStarred(selectedEmails, !allStarred); clearSelection(); }}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
hitSlop={6}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={allStarred ? t('context_menu.unstar', 'Unstar') : t('context_menu.star', 'Star')}
|
||||||
|
>
|
||||||
<Star size={20} color={allStarred ? c.starred : c.text} fill={allStarred ? c.starred : 'transparent'} />
|
<Star size={20} color={allStarred ? c.starred : c.text} fill={allStarred ? c.starred : 'transparent'} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Pressable onPress={() => { setRead(selectedEmails, !allRead); clearSelection(); }} style={styles.headerBtn} hitSlop={6}>
|
<Pressable
|
||||||
|
onPress={() => { setRead(selectedEmails, !allRead); clearSelection(); }}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
hitSlop={6}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={allRead
|
||||||
|
? t('email_list.batch_actions.mark_unread', 'Mark as unread')
|
||||||
|
: t('email_list.batch_actions.mark_read', 'Mark as read')}
|
||||||
|
>
|
||||||
{allRead ? <MailIcon size={20} color={c.text} /> : <MailOpen size={20} color={c.text} />}
|
{allRead ? <MailIcon size={20} color={c.text} /> : <MailOpen size={20} color={c.text} />}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{role !== 'sent' && role !== 'drafts' && (
|
{role !== 'sent' && role !== 'drafts' && (
|
||||||
<Pressable onPress={() => { spam(selectedEmails); clearSelection(); }} style={styles.headerBtn} hitSlop={6}>
|
<Pressable
|
||||||
|
onPress={() => { spam(selectedEmails); clearSelection(); }}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
hitSlop={6}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={role === 'junk' ? t('context_menu.not_spam', 'Not spam') : t('context_menu.mark_as_spam', 'Report spam')}
|
||||||
|
>
|
||||||
{role === 'junk' ? <ShieldCheck size={20} color={c.text} /> : <ShieldAlert size={20} color={c.text} />}
|
{role === 'junk' ? <ShieldCheck size={20} color={c.text} /> : <ShieldAlert size={20} color={c.text} />}
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
{role !== 'archive' && (
|
{role !== 'archive' && (
|
||||||
<Pressable onPress={() => { archive(selectedEmails); clearSelection(); }} style={styles.headerBtn} hitSlop={6}>
|
<Pressable
|
||||||
|
onPress={() => { archive(selectedEmails); clearSelection(); }}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
hitSlop={6}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('context_menu.archive', 'Archive')}
|
||||||
|
>
|
||||||
<Archive size={20} color={c.text} />
|
<Archive size={20} color={c.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
<Pressable onPress={() => { void remove(selectedEmails).then(clearSelection); }} style={styles.headerBtn} hitSlop={6}>
|
<Pressable
|
||||||
|
onPress={() => { void remove(selectedEmails).then(clearSelection); }}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
hitSlop={6}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('email_list.batch_actions.delete', 'Delete')}
|
||||||
|
>
|
||||||
<Trash2 size={20} color={c.text} />
|
<Trash2 size={20} color={c.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
</View>
|
</View>
|
||||||
) : (
|
) : (
|
||||||
<View style={styles.header}>
|
<View style={styles.header}>
|
||||||
<Pressable onPress={() => navigation.goBack()} style={styles.headerBtn} hitSlop={8}>
|
<Pressable
|
||||||
|
onPress={() => navigation.goBack()}
|
||||||
|
style={styles.headerBtn}
|
||||||
|
hitSlop={8}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('common.back', 'Back')}
|
||||||
|
>
|
||||||
<ArrowLeft size={22} color={c.text} />
|
<ArrowLeft size={22} color={c.text} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
<Text style={styles.headerTitle} numberOfLines={1}>{title}</Text>
|
<Text style={styles.headerTitle} numberOfLines={1}>{title}</Text>
|
||||||
@@ -436,7 +480,12 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
|||||||
returnKeyType="search"
|
returnKeyType="search"
|
||||||
/>
|
/>
|
||||||
{searchInput.length > 0 && (
|
{searchInput.length > 0 && (
|
||||||
<Pressable onPress={() => { setSearchInput(''); setQuery(''); }} hitSlop={8}>
|
<Pressable
|
||||||
|
onPress={() => { setSearchInput(''); setQuery(''); }}
|
||||||
|
hitSlop={8}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel={t('contacts.clear_search', 'Clear search')}
|
||||||
|
>
|
||||||
<X size={14} color={c.textMuted} />
|
<X size={14} color={c.textMuted} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
)}
|
)}
|
||||||
@@ -446,7 +495,7 @@ export default function UnifiedInboxScreen({ navigation, route }: Props) {
|
|||||||
<View style={styles.errorBanner}>
|
<View style={styles.errorBanner}>
|
||||||
<AlertTriangle size={14} color={c.error} />
|
<AlertTriangle size={14} color={c.error} />
|
||||||
<Text style={styles.errorBannerText} numberOfLines={2}>
|
<Text style={styles.errorBannerText} numberOfLines={2}>
|
||||||
{t('unified_mailbox.accounts_failed', `${errorCount} account(s) could not be loaded`, { count: errorCount })}
|
{t('unified_mailbox.accounts_failed', '{count, plural, one {# account could not be loaded} other {# accounts could not be loaded}}', { count: errorCount })}
|
||||||
{': '}
|
{': '}
|
||||||
{Object.values(errors)[0]}
|
{Object.values(errors)[0]}
|
||||||
</Text>
|
</Text>
|
||||||
|
|||||||
Reference in New Issue
Block a user