import React from 'react'; import { View, Text, StyleSheet, ScrollView, Pressable, Modal, Animated, Easing, } from 'react-native'; import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { Inbox, Send, File as FileIcon, Trash2, Ban, Archive, Folder, X, Check, type AppIcon, } from './icons'; import { spacing, radius, typography, type ThemePalette } from '../theme/tokens'; import { useColors } from '../theme/colors'; import { buildMailboxTree, flattenVisible, type MailboxNode } from '../lib/mailbox-tree'; import { useSheetDrag } from '../lib/use-sheet-drag'; import { useLocaleStore } from '../stores/locale-store'; import { localizeMailboxName } from '../lib/mailbox-label'; import type { Mailbox } from '../api/types'; import { MODAL_ORIENTATIONS } from '../lib/modal-orientations'; import { useSheetInsets } from '../lib/use-layout'; function moveTargetIcon(role: string | null | undefined, name: string): AppIcon { const lower = name.toLowerCase(); if (role === 'inbox' || lower.includes('inbox')) return Inbox; if (role === 'sent' || lower.includes('sent')) return Send; if (role === 'drafts' || lower.includes('draft')) return FileIcon; if (role === 'trash' || lower.includes('trash') || lower.includes('deleted')) return Trash2; if (role === 'junk' || role === 'spam' || lower.includes('junk') || lower.includes('spam')) return Ban; if (role === 'archive' || lower.includes('archive')) return Archive; return Folder; } interface MoveSheetProps { visible: boolean; onClose: () => void; mailboxes: Mailbox[]; /** Folder the email is currently in - shown with a check, not selectable. */ currentMailboxId: string | null; onPick: (id: string) => void; } export function MoveSheet({ visible, onClose, mailboxes, currentMailboxId, onPick, }: MoveSheetProps) { const c = useColors(); const styles = React.useMemo(() => makeStyles(c), [c]); const t = useLocaleStore((s) => s.t); const insets = useSafeAreaInsets(); const slideY = React.useRef(new Animated.Value(500)).current; // Centred on a wide window, clear of side insets in landscape. const sheetInsets = useSheetInsets(); const overlayOpacity = React.useRef(new Animated.Value(0)).current; const dragHandlers = useSheetDrag({ slideY, closedY: 500, onClose }); React.useEffect(() => { if (visible) { Animated.parallel([ Animated.timing(slideY, { toValue: 0, duration: 220, easing: Easing.out(Easing.cubic), useNativeDriver: true }), Animated.timing(overlayOpacity, { toValue: 1, duration: 220, useNativeDriver: true }), ]).start(); } else { Animated.parallel([ Animated.timing(slideY, { toValue: 500, duration: 180, easing: Easing.in(Easing.cubic), useNativeDriver: true }), Animated.timing(overlayOpacity, { toValue: 0, duration: 180, useNativeDriver: true }), ]).start(); } }, [visible, slideY, overlayOpacity]); const visibleNodes = React.useMemo(() => { const tree = buildMailboxTree(mailboxes); const expanded = new Set(); const collect = (nodes: MailboxNode[]) => { for (const n of nodes) { if (n.children.length > 0) { expanded.add(n.id); collect(n.children); } } }; collect(tree); return flattenVisible(tree, expanded); }, [mailboxes]); return ( {t('context_menu.move_to', 'Move to folder')} {visibleNodes.map((node) => { const Icon = moveTargetIcon(node.role, node.name); const isCurrent = node.id === currentMailboxId; // A shared account's header is a grouping row, not a folder, and // Drafts is never a move target (the webmail excludes it too). const canTarget = !node.isAccountNode && node.myRights?.mayAddItems !== false && !isCurrent && node.role !== 'drafts'; return ( onPick(node.id) : undefined} disabled={!canTarget} style={({ pressed }) => [ styles.moveRow, pressed && canTarget && styles.moveRowPressed, { paddingLeft: spacing.lg + node.depth * 16 }, ]} > {node.isAccountNode ? node.name : localizeMailboxName(node.role, node.name, t)} {isCurrent && } ); })} ); } function makeStyles(c: ThemePalette) { return StyleSheet.create({ sheetOverlay: { ...StyleSheet.absoluteFillObject, backgroundColor: 'rgba(0,0,0,0.5)', }, sheetOverlayPress: { flex: 1 }, sheet: { position: 'absolute', left: 0, right: 0, bottom: 0, maxHeight: '75%', backgroundColor: c.popover, borderTopLeftRadius: radius.lg, borderTopRightRadius: radius.lg, borderTopWidth: 1, borderColor: c.border, paddingTop: spacing.sm, }, sheetHandleHit: { alignItems: 'center', paddingTop: spacing.xs, paddingBottom: spacing.sm, }, sheetHandle: { width: 36, height: 4, borderRadius: 2, backgroundColor: c.border, }, sheetHeader: { flexDirection: 'row', alignItems: 'center', justifyContent: 'space-between', paddingHorizontal: spacing.lg, paddingBottom: spacing.sm, borderBottomWidth: 1, borderBottomColor: c.border, }, sheetTitle: { ...typography.bodySemibold, color: c.text, }, sheetClose: { width: 28, height: 28, alignItems: 'center', justifyContent: 'center', borderRadius: radius.xs, }, moveRow: { flexDirection: 'row', alignItems: 'center', gap: spacing.sm, paddingRight: spacing.lg, paddingVertical: spacing.sm + 2, minHeight: 44, }, moveRowPressed: { backgroundColor: c.surfaceHover }, moveRowLabel: { ...typography.body, color: c.text, flex: 1, }, moveRowLabelDisabled: { color: c.textMuted, }, }); }