diff --git a/src/components/SwipeableRow.tsx b/src/components/SwipeableRow.tsx index f5e76c88..8360d7bc 100644 --- a/src/components/SwipeableRow.tsx +++ b/src/components/SwipeableRow.tsx @@ -172,9 +172,9 @@ export function SwipeableRow({ return; } // Instant mode: fire on release-past-threshold, no second tap. - if (g.dx >= COMMIT_THRESHOLD && rightAction !== 'none') { + if ((g.dx >= COMMIT_THRESHOLD || (g.dx > 40 && g.vx >= 0.5)) && rightAction !== 'none') { fly(widthRef.current || EXIT_DISTANCE, rightAction); - } else if (g.dx <= -COMMIT_THRESHOLD && leftAction !== 'none') { + } else if ((g.dx <= -COMMIT_THRESHOLD || (g.dx < -40 && g.vx <= -0.5)) && leftAction !== 'none') { fly(-(widthRef.current || EXIT_DISTANCE), leftAction); } else { Animated.spring(dx, { toValue: 0, useNativeDriver: true, speed: 24, bounciness: 4 }).start(); diff --git a/src/lib/mailbox-tree.ts b/src/lib/mailbox-tree.ts index d36dcdd7..1d307d11 100644 --- a/src/lib/mailbox-tree.ts +++ b/src/lib/mailbox-tree.ts @@ -96,3 +96,36 @@ export function flattenVisible( walk(nodes); return out; } + +export function findTrashMailbox(mailboxes: Mailbox[]): Mailbox | undefined { + const roleMatch = mailboxes.find((m) => m.role === 'trash'); + if (roleMatch) return roleMatch; + + const names = ['trash', 'bin', 'deleted', 'deleted items', 'corbeille', 'messages supprimés', 'éléments supprimés', 'supprimé', 'supprimés']; + return mailboxes.find((m) => { + const lower = m.name.toLowerCase(); + return names.includes(lower) || names.some((n) => lower.includes(n)); + }); +} + +export function findArchiveMailbox(mailboxes: Mailbox[]): Mailbox | undefined { + const roleMatch = mailboxes.find((m) => m.role === 'archive'); + if (roleMatch) return roleMatch; + + const names = ['archive', 'archives', 'archived']; + return mailboxes.find((m) => { + const lower = m.name.toLowerCase(); + return names.includes(lower) || names.some((n) => lower.includes(n)); + }); +} + +export function findJunkMailbox(mailboxes: Mailbox[]): Mailbox | undefined { + const roleMatch = mailboxes.find((m) => m.role === 'junk' || m.role === 'spam'); + if (roleMatch) return roleMatch; + + const names = ['junk', 'spam', 'indésirables', 'indésirable', 'courrier indésirable']; + return mailboxes.find((m) => { + const lower = m.name.toLowerCase(); + return names.includes(lower) || names.some((n) => lower.includes(n)); + }); +} diff --git a/src/screens/EmailListScreen.tsx b/src/screens/EmailListScreen.tsx index 8e6944ea..a713daee 100644 --- a/src/screens/EmailListScreen.tsx +++ b/src/screens/EmailListScreen.tsx @@ -24,8 +24,8 @@ import { useSettingsStore, type SwipeAction } from '../stores/settings-store'; import { useKeywordsStore } from '../stores/keywords-store'; import { useLocaleStore } from '../stores/locale-store'; import { formatListDate } from '../lib/date-format'; +import { findTrashMailbox, findArchiveMailbox, findJunkMailbox } from '../lib/mailbox-tree'; import type { Email } from '../api/types'; - function getSenderName(email: Email): string { return email.from?.[0]?.name || email.from?.[0]?.email || 'Unknown'; } @@ -164,6 +164,7 @@ interface EmailListScreenProps { export default function EmailListScreen({ onEmailPress, onComposePress }: EmailListScreenProps) { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); + const { t } = useLocaleStore(); const [drawerOpen, setDrawerOpen] = React.useState(false); const [filterMenuOpen, setFilterMenuOpen] = React.useState(false); const emails = useEmailStore((s) => s.emails); @@ -232,15 +233,15 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL }, [emails, disableThreading]); const archiveMailboxId = React.useMemo( - () => mailboxes.find((m) => m.role === 'archive')?.id ?? null, + () => findArchiveMailbox(mailboxes)?.id ?? null, [mailboxes], ); const trashMailboxId = React.useMemo( - () => mailboxes.find((m) => m.role === 'trash')?.id ?? null, + () => findTrashMailbox(mailboxes)?.id ?? null, [mailboxes], ); const junkMailboxId = React.useMemo( - () => mailboxes.find((m) => m.role === 'junk' || m.role === 'spam')?.id ?? null, + () => findJunkMailbox(mailboxes)?.id ?? null, [mailboxes], ); @@ -336,16 +337,31 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL case 'archive': if (archiveMailboxId && currentMailboxId !== archiveMailboxId) { void archiveEmailAction(id); + } else if (!archiveMailboxId) { + Alert.alert( + t('email_list.error', 'Error'), + t('email_list.no_archive_folder', 'Could not find an Archive folder on the server.'), + ); } break; case 'delete': if (trashMailboxId) { void deleteEmailAction(id, trashMailboxId, currentMailboxId); + } else { + Alert.alert( + t('email_list.error', 'Error'), + t('email_list.no_trash_folder', 'Could not find a Trash folder on the server. Please check your mailbox configuration.'), + ); } break; case 'spam': if (junkMailboxId && currentMailboxId !== junkMailboxId) { void moveToMailboxAction(id, currentMailboxId, junkMailboxId); + } else if (!junkMailboxId) { + Alert.alert( + t('email_list.error', 'Error'), + t('email_list.no_junk_folder', 'Could not find a Spam/Junk folder on the server.'), + ); } break; case 'read': @@ -365,7 +381,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL }, [ currentMailboxId, archiveMailboxId, trashMailboxId, junkMailboxId, moveToMailboxAction, archiveEmailAction, deleteEmailAction, - markRead, markUnread, toggleStar, togglePin, + markRead, markUnread, toggleStar, togglePin, t, ]); const renderEmailRow = React.useCallback( @@ -433,7 +449,7 @@ export default function EmailListScreen({ onEmailPress, onComposePress }: EmailL }; const handleBulkDelete = async () => { - const trash = mailboxes.find((m) => m.role === 'trash'); + const trash = findTrashMailbox(mailboxes); if (!trash || !currentMailboxId) { clearSelection(); return; diff --git a/src/screens/EmailThreadScreen.tsx b/src/screens/EmailThreadScreen.tsx index 52ea31ee..14632870 100644 --- a/src/screens/EmailThreadScreen.tsx +++ b/src/screens/EmailThreadScreen.tsx @@ -29,6 +29,8 @@ import { setEmailKeywords } from '../api/email'; import { shareEmailEml, shareAttachment, downloadAttachment } from '../lib/email-export'; import { useKeywordsStore, keywordToken, type KeywordDef } from '../stores/keywords-store'; import { useSheetDrag } from '../lib/use-sheet-drag'; +import { useLocaleStore } from '../stores/locale-store'; +import { findTrashMailbox } from '../lib/mailbox-tree'; import type { Email, Mailbox } from '../api/types'; import type { RootStackParamList } from '../navigation/types'; @@ -65,6 +67,7 @@ function plainTextBody(email: Email): string { export default function EmailThreadScreen({ route, navigation }: Props) { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); + const { t } = useLocaleStore(); const { jmapAccountId } = route.params; // The displayed email is tracked in local state (not a route param) so that // swiping / Prev-Next can switch messages in place without remounting the @@ -299,8 +302,14 @@ export default function EmailThreadScreen({ route, navigation }: Props) { const onDelete = () => { if (!email || !currentMailboxId) return; - const trash = mailboxes.find((m) => m.role === 'trash'); - if (!trash) return; + const trash = findTrashMailbox(mailboxes); + if (!trash) { + Alert.alert( + t('email_list.error', 'Error'), + t('email_list.no_trash_folder', 'Could not find a Trash folder on the server. Please check your mailbox configuration.'), + ); + return; + } void deleteEmail(email.id, trash.id, currentMailboxId); navigation.goBack(); };