import { useState } from 'react'; import { ActivityIndicator, LayoutAnimation, Linking, Modal, Platform, TouchableOpacity, Text, UIManager, View, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { ConfirmAlert } from '../ConfirmAlert'; import { EditMailAccountSheet } from './EditMailAccountSheet'; import { EditMailTitleSheet } from './EditMailTitleSheet'; import { useMailInterval } from '../../hooks/useMailInterval'; import type { MailAccount } from '../../hooks/useMailStatus'; if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) { UIManager.setLayoutAnimationEnabledExperimental(true); } type Props = { account: MailAccount; plan: 'free' | 'pro' | 'legend'; expanded: boolean; onToggle: () => void; onDisconnect: (id: string) => Promise; onIntervalChanged: () => void; onEditSuccess: () => void; disconnecting?: boolean; }; function PausedBadge({ t }: { t: (k: string) => string }) { return ( {t('plan_limit.mail_account_paused')} ); } function resolveProviderIcon(provider: string): { icon: React.ComponentProps['name']; color: string; } { const p = provider.toLowerCase(); if (p.includes('gmail') || p.includes('google')) return { icon: 'mail', color: '#EA4335' }; if (p.includes('icloud') || p.includes('apple')) return { icon: 'cloud', color: '#007AFF' }; if (p.includes('outlook') || p.includes('hotmail') || p.includes('microsoft')) return { icon: 'mail-open', color: '#0078D4' }; if (p.includes('yahoo')) return { icon: 'at', color: '#7C3AED' }; if (p.includes('gmx') || p.includes('web.de')) return { icon: 'mail-unread', color: '#E87A22' }; return { icon: 'server', color: '#737373' }; } function isOAuthProvider(provider: string): boolean { return provider === 'outlook_oauth'; } const STALE_THRESHOLD_MS = 5 * 60 * 1_000; const IDLE_HEARTBEAT_STALE_MS = STALE_THRESHOLD_MS; const NO_NEW_MAIL_THRESHOLD_MS = 60 * 60_000; function formatRelativeAbsolute(ts: Date): string { const min = Math.floor((Date.now() - ts.getTime()) / 60_000); const todayStr = new Date().toDateString(); const yesterdayStr = new Date(Date.now() - 86_400_000).toDateString(); const hh = ts.getHours().toString().padStart(2, '0'); const mm = ts.getMinutes().toString().padStart(2, '0'); let dayLabel: string; if (ts.toDateString() === todayStr) dayLabel = 'heute'; else if (ts.toDateString() === yesterdayStr) dayLabel = 'gestern'; else dayLabel = ts.toLocaleDateString('de', { day: '2-digit', month: '2-digit' }); let rel: string; if (min < 1) rel = 'gerade eben'; else if (min < 60) rel = `vor ${min} min`; else if (min < 1440) rel = `vor ${Math.floor(min / 60)}h`; else rel = `vor ${Math.floor(min / 1440)}d`; return `${rel} (${dayLabel} ${hh}:${mm})`; } function idleHeartbeatAlive(lastIdleHeartbeatAt: string | null | undefined): boolean { if (!lastIdleHeartbeatAt) return false; return Date.now() - new Date(lastIdleHeartbeatAt).getTime() < IDLE_HEARTBEAT_STALE_MS; } function StatusBadgeRow({ account, isLegend, t, }: { account: MailAccount; isLegend: boolean; t: (k: string, opts?: Record) => string; }) { if (account.lastConnectError) { const isAuthError = account.lastConnectError.toLowerCase().includes('invalid credentials') || account.lastConnectError.toLowerCase().includes('authentication failed'); const errorLabel = isAuthError ? t('mail.status_auth_error') : t('mail.status_connect_error'); const since = account.lastConnectErrorAt ? formatRelativeAbsolute(new Date(account.lastConnectErrorAt)) : null; return ( {errorLabel} · {t('mail.status_error_tap_hint')} {since ? ( {since} ) : null} ); } if (!account.lastScannedAt) { return ( {t('mail.status_waiting_first_connect')} ); } const heartbeatAlive = idleHeartbeatAlive(account.lastIdleHeartbeatAt); const lastScannedTs = new Date(account.lastScannedAt); const scannedAgo = Date.now() - lastScannedTs.getTime(); const scannedRelAbs = formatRelativeAbsolute(lastScannedTs); if (!heartbeatAlive && scannedAgo > STALE_THRESHOLD_MS) { return ( {t('mail.status_stale')} {t('mail.status_stale_last_scan', { rel: scannedRelAbs })} ); } if (heartbeatAlive) { const heartbeatTs = new Date(account.lastIdleHeartbeatAt!); const heartbeatMin = Math.floor((Date.now() - heartbeatTs.getTime()) / 60_000); const idleSince = heartbeatMin < 1 ? 'gerade eben' : `${heartbeatMin} min`; if (scannedAgo > NO_NEW_MAIL_THRESHOLD_MS) { return ( {isLegend ? t('mail.live') : t('mail.account_active')} {t('mail.status_live_no_new_mail', { rel: scannedRelAbs })} ); } return ( {isLegend ? t('mail.live') : t('mail.account_active')} {t('mail.status_live_idle', { rel: idleSince })} ); } return ( {isLegend ? t('mail.live') : t('mail.account_active')} {formatRelativeAbsolute(new Date(account.lastScannedAt!))} ); } const INTERVAL_OPTIONS_BY_PLAN: Record<'free' | 'pro' | 'legend', number[]> = { free: [4], pro: [1, 4, 8], legend: [1, 4, 8], }; function maskEmail(email: string): string { const [local, domain] = email.split('@'); if (!local || !domain) return email; if (local.length <= 3) return `${local[0]}***@${domain}`; return `${local.slice(0, 3)}***@${domain}`; } function domainFromEmail(email: string): string { return email.split('@')[1] ?? email; } function SettingsRow({ icon, label, value, onPress, destructive, }: { icon: React.ComponentProps['name']; label: string; value?: string; onPress?: () => void; destructive?: boolean; }) { const color = destructive ? '#dc2626' : '#0a0a0a'; const Wrapper = onPress ? TouchableOpacity : View; const wrapperProps = onPress ? { activeOpacity: 0.7, onPress } : {}; return ( {label} {value !== undefined && ( {value} )} {onPress && !destructive && ( )} ); } function OAuthDisconnectHintModal({ visible, onClose, t, }: { visible: boolean; onClose: () => void; t: (key: string) => string; }) { return ( {}} style={{ width: '88%', maxWidth: 340 }}> {t('mail.oauth.disconnect_hint_title')} {t('mail.oauth.disconnect_hint_body')} Linking.openURL('https://account.microsoft.com/consent').catch(() => {})} style={{ flex: 1, paddingVertical: 10, borderRadius: 10, backgroundColor: '#eff6ff', borderWidth: 1, borderColor: '#bfdbfe', alignItems: 'center', }} > {t('mail.oauth.disconnect_hint_open_ms')} OK ); } export function MailAccountCard({ account, plan, expanded, onToggle, onDisconnect, onIntervalChanged, onEditSuccess, disconnecting, }: Props) { const { t } = useTranslation(); const [confirmVisible, setConfirmVisible] = useState(false); const [editPasswordVisible, setEditPasswordVisible] = useState(false); const [editTitleVisible, setEditTitleVisible] = useState(false); const [oauthDisconnectHintVisible, setOauthDisconnectHintVisible] = useState(false); const [localTitle, setLocalTitle] = useState(account.title ?? null); const { setInterval, updating } = useMailInterval(); const { icon, color } = resolveProviderIcon(account.provider); const isOAuth = isOAuthProvider(account.provider); const isLegend = plan === 'legend'; const isPaused = account.paused === true; const intervalOptions = INTERVAL_OPTIONS_BY_PLAN[plan]; const displayTitle = localTitle ?? domainFromEmail(account.email); const subEmail = maskEmail(account.email); function handleToggle() { if (account.lastConnectError) { setEditPasswordVisible(true); return; } LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut); onToggle(); } async function handleSetInterval(value: number) { const res = await setInterval(account.id, value); if (res.ok) onIntervalChanged(); } function handleTitleSaved(newTitle: string | null) { setLocalTitle(newTitle); onEditSuccess(); } return ( <> {/* Header — always visible, tap to expand settings */} {/* Title — prominent */} {displayTitle} {/* Email — small sub-label */} {subEmail} {isPaused ? : } {/* Collapsible: Settings section */} {expanded && ( {/* Stats banner */} {t('mail.account_stat_blocked')} {account.totalBlocked.toLocaleString()} {t('mail.account_of_scanned', { scanned: account.totalScanned.toLocaleString(), })} {/* Scan interval (non-legend) */} {isLegend ? ( {t('mail.realtime_desc')} ) : ( {t('mail.scan_interval_label')} {intervalOptions.map((opt, idx) => { const active = account.scanInterval === opt; const disabled = plan === 'free' || updating === account.id; return ( handleSetInterval(opt)} style={{ flex: 1, paddingVertical: 9, borderRadius: 10, alignItems: 'center', backgroundColor: active ? '#007AFF' : '#f5f5f5', marginLeft: idx === 0 ? 0 : 6, opacity: disabled && !active ? 0.5 : 1, }} > {opt}h ); })} {plan === 'free' && ( {t('mail.free_scan_interval_hint')} )} )} {/* Settings separator label */} {t('mail.settings_section_label')} {/* Settings rows */} setEditTitleVisible(true)} /> {!isOAuth && ( setEditPasswordVisible(true)} /> )} setConfirmVisible(true)} disabled={disconnecting} style={{ flexDirection: 'row', alignItems: 'center', paddingVertical: 12, paddingHorizontal: 14, borderTopWidth: 1, borderTopColor: '#f5f5f5', opacity: disconnecting ? 0.5 : 1, }} > {disconnecting ? ( ) : ( )} {t('mail.row_disconnect')} )} { setConfirmVisible(false); await onDisconnect(account.id); if (isOAuth) setOauthDisconnectHintVisible(true); }} onCancel={() => setConfirmVisible(false)} /> setOauthDisconnectHintVisible(false)} t={t} /> {!isOAuth && ( setEditPasswordVisible(false)} onSuccess={onEditSuccess} /> )} setEditTitleVisible(false)} onSuccess={handleTitleSaved} /> ); }