diff --git a/apps/mobile-app/app/(tabs)/emails/index.tsx b/apps/mobile-app/app/(tabs)/emails/index.tsx
index 5cc218699..e7c8d385c 100644
--- a/apps/mobile-app/app/(tabs)/emails/index.tsx
+++ b/apps/mobile-app/app/(tabs)/emails/index.tsx
@@ -13,7 +13,7 @@ import emitter from '@/utils/EventEmitter';
import { useColors } from '@/hooks/useColorScheme';
import { useMinDurationLoading } from '@/hooks/useMinDurationLoading';
-import { EmailCard } from '@/components/EmailCard';
+import { SwipeableEmailCard } from '@/components/SwipeableEmailCard';
import { ThemedContainer } from '@/components/themed/ThemedContainer';
import { ThemedText } from '@/components/themed/ThemedText';
import { CollapsibleHeader } from '@/components/ui/CollapsibleHeader';
@@ -216,7 +216,7 @@ export default function EmailsScreen() : React.ReactNode {
}
return emails.map((email) => (
-
+
));
};
diff --git a/apps/mobile-app/app/_layout.tsx b/apps/mobile-app/app/_layout.tsx
index b4be22443..f05743898 100644
--- a/apps/mobile-app/app/_layout.tsx
+++ b/apps/mobile-app/app/_layout.tsx
@@ -187,7 +187,7 @@ export default function RootLayout() : React.ReactNode {
-
+
diff --git a/apps/mobile-app/components/SwipeableEmailCard.tsx b/apps/mobile-app/components/SwipeableEmailCard.tsx
new file mode 100644
index 000000000..bdac85c3f
--- /dev/null
+++ b/apps/mobile-app/components/SwipeableEmailCard.tsx
@@ -0,0 +1,207 @@
+import { useRef } from 'react';
+import { StyleSheet, View, Animated, Alert } from 'react-native';
+import { Swipeable } from 'react-native-gesture-handler';
+import * as Haptics from 'expo-haptics';
+
+import type { MailboxEmail } from '@/utils/dist/shared/models/webapi';
+
+import { useColors } from '@/hooks/useColorScheme';
+import { useTranslation } from '@/hooks/useTranslation';
+import { useWebApi } from '@/context/WebApiContext';
+
+import { EmailCard } from '@/components/EmailCard';
+import { RobustPressable } from '@/components/ui/RobustPressable';
+import { IconSymbol } from '@/components/ui/IconSymbol';
+import { IconSymbolName } from '@/components/ui/IconSymbolName';
+import { emitter } from '@/utils/EventEmitter';
+
+type SwipeableEmailCardProps = {
+ email: MailboxEmail;
+ onDelete?: (emailId: string) => void;
+};
+
+/**
+ * Swipeable email card component with swipe-to-delete functionality.
+ * Swiping left or right reveals a delete button for 2-step deletion.
+ */
+export function SwipeableEmailCard({ email, onDelete }: SwipeableEmailCardProps): React.ReactNode {
+ const colors = useColors();
+ const { t } = useTranslation();
+ const webApi = useWebApi();
+ const swipeableRef = useRef(null);
+
+ /**
+ * Handle email deletion.
+ */
+ const handleDelete = async (): Promise => {
+ // Trigger haptic feedback
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium);
+
+ // Show confirmation alert
+ Alert.alert(
+ t('emails.deleteEmail'),
+ t('emails.deleteEmailConfirm'),
+ [
+ {
+ text: t('common.cancel'),
+ style: 'cancel',
+ onPress: () => {
+ // Close the swipeable
+ swipeableRef.current?.close();
+ },
+ },
+ {
+ text: t('common.delete'),
+ style: 'destructive',
+ onPress: async (): Promise => {
+ try {
+ // Trigger haptic feedback for deletion
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Success);
+
+ // Delete the email from the server
+ await webApi.delete(`Email/${email.id}`);
+
+ // Close the swipeable
+ swipeableRef.current?.close();
+
+ // Notify parent or refresh the list
+ if (onDelete) {
+ onDelete(email.id);
+ } else {
+ emitter.emit('refreshEmails');
+ }
+ } catch (err) {
+ // Trigger error haptic feedback
+ await Haptics.notificationAsync(Haptics.NotificationFeedbackType.Error);
+
+ // Show error alert
+ Alert.alert(
+ t('common.error'),
+ err instanceof Error ? err.message : t('emails.errors.deleteFailed')
+ );
+
+ // Close the swipeable
+ swipeableRef.current?.close();
+ }
+ },
+ },
+ ]
+ );
+ };
+
+ /**
+ * Render the right action (delete button) when swiping left.
+ */
+ const renderRightActions = (
+ progress: Animated.AnimatedInterpolation,
+ dragX: Animated.AnimatedInterpolation
+ ): React.ReactNode => {
+ const translateX = dragX.interpolate({
+ inputRange: [-80, 0],
+ outputRange: [0, 80],
+ extrapolate: 'clamp',
+ });
+
+ const opacity = progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ extrapolate: 'clamp',
+ });
+
+ return (
+
+
+
+
+
+ );
+ };
+
+ /**
+ * Render the left action (delete button) when swiping right.
+ */
+ const renderLeftActions = (
+ progress: Animated.AnimatedInterpolation,
+ dragX: Animated.AnimatedInterpolation
+ ): React.ReactNode => {
+ const translateX = dragX.interpolate({
+ inputRange: [0, 80],
+ outputRange: [-80, 0],
+ extrapolate: 'clamp',
+ });
+
+ const opacity = progress.interpolate({
+ inputRange: [0, 1],
+ outputRange: [0, 1],
+ extrapolate: 'clamp',
+ });
+
+ return (
+
+
+
+
+
+ );
+ };
+
+ /**
+ * Handle swipe begin to provide haptic feedback.
+ */
+ const handleSwipeableWillOpen = async (): Promise => {
+ await Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Light);
+ };
+
+ const styles = StyleSheet.create({
+ deleteAction: {
+ alignItems: 'center',
+ flexDirection: 'row',
+ justifyContent: 'center',
+ marginBottom: 12,
+ },
+ deleteButton: {
+ alignItems: 'center',
+ borderRadius: 8,
+ height: '100%',
+ justifyContent: 'center',
+ width: 80,
+ },
+ });
+
+ return (
+
+
+
+ );
+}
diff --git a/apps/mobile-app/components/ui/IconSymbol.ios.tsx b/apps/mobile-app/components/ui/IconSymbol.ios.tsx
index b87900e2e..de5da40c7 100644
--- a/apps/mobile-app/components/ui/IconSymbol.ios.tsx
+++ b/apps/mobile-app/components/ui/IconSymbol.ios.tsx
@@ -15,6 +15,7 @@ const SF_SYMBOLS_MAPPING: Record