Merge pull request #24 from arnauda-gh/fix/swipe-actions-notification

fix(mailboxes): prevent silent swipe failures by notifying user of missing folders
This commit is contained in:
Linus Rath
2026-07-16 17:55:05 +02:00
committed by GitHub
4 changed files with 68 additions and 10 deletions
+2 -2
View File
@@ -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();
+33
View File
@@ -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));
});
}
+22 -6
View File
@@ -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;
+11 -2
View File
@@ -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();
};