diff --git a/apps/rebreak-native/app/(app)/mail.tsx b/apps/rebreak-native/app/(app)/mail.tsx index 40d6558..9b67655 100644 --- a/apps/rebreak-native/app/(app)/mail.tsx +++ b/apps/rebreak-native/app/(app)/mail.tsx @@ -11,7 +11,6 @@ import { View, } from 'react-native'; import { useBottomTabBarHeight } from 'react-native-bottom-tabs'; -import { useSafeAreaInsets } from 'react-native-safe-area-context'; import { useTranslation } from 'react-i18next'; import { Ionicons } from '@expo/vector-icons'; import { AppHeader } from '../../components/AppHeader'; @@ -184,7 +183,6 @@ function MoreInfosSection({ export default function MailScreen() { const { t } = useTranslation(); const tabBarHeight = useBottomTabBarHeight(); - const insets = useSafeAreaInsets(); const colors = useColors(); const { plan } = useUserPlan(); @@ -265,7 +263,7 @@ export default function MailScreen() { contentContainerStyle={{ paddingHorizontal: 16, paddingTop: 16, - paddingBottom: tabBarHeight + 88, + paddingBottom: tabBarHeight + 24, }} showsVerticalScrollIndicator={false} > @@ -365,32 +363,59 @@ export default function MailScreen() { {/* 2. ACCOUNT LIST */} {hasAccounts && ( - - {t('mail.section_accounts')} - - - {maxAccounts === Infinity - ? t('mail.section_accounts_count_unlimited', { used: accounts.length }) - : t('mail.section_accounts_count', { - used: accounts.length, - max: maxAccounts, - })} - + + + + {t('mail.section_accounts')} + + + {maxAccounts === Infinity + ? t('mail.section_accounts_count_unlimited', { used: accounts.length }) + : t('mail.section_accounts_count', { + used: accounts.length, + max: maxAccounts, + })} + + + + + + {t('mail.add_account')} + + + )} @@ -442,31 +467,6 @@ export default function MailScreen() { )} - {/* 5. FAB — Floating Action Button */} - - - - setSheetVisible(false)} diff --git a/apps/rebreak-native/components/mail/MailActivityLog.tsx b/apps/rebreak-native/components/mail/MailActivityLog.tsx index 4a152e3..4e1493c 100644 --- a/apps/rebreak-native/components/mail/MailActivityLog.tsx +++ b/apps/rebreak-native/components/mail/MailActivityLog.tsx @@ -23,14 +23,16 @@ type Props = { providers?: string[]; }; -function formatDate(iso: string, t: (k: string) => string): string { - const diff = Date.now() - new Date(iso).getTime(); +function formatDate(iso: string, t: (k: string) => string): string | null { + const ts = new Date(iso).getTime(); + if (!Number.isFinite(ts)) return null; + const diff = Date.now() - ts; const mins = Math.floor(diff / 60_000); if (mins < 2) return t('mail.account_just_now'); if (mins < 60) return `${mins} min`; const hours = Math.floor(mins / 60); - if (hours < 24) return `${hours}h`; - return `${Math.floor(hours / 24)}d`; + if (hours < 24) return `vor ${hours}h`; + return `vor ${Math.floor(hours / 24)}d`; } function domainFromEmail(email: string): string { @@ -228,87 +230,40 @@ function ActivityItem({ t: (k: string, opts?: any) => string; colors: ReturnType; }) { - const accountLabel = item.connection_title ?? ( - item.sender_email ? domainFromEmail(item.sender_email) : null + const providerLabel = item.connection?.providerLabel ?? ( + item.senderEmail ? domainFromEmail(item.senderEmail) : null ); + const timeLabel = formatDate(item.receivedAt, t); + const subLine = [timeLabel, providerLabel].filter(Boolean).join(' · '); return ( - - - - - - - {item.subject || t('mail.activity_no_subject')} - - {accountLabel && ( - - - {accountLabel} - - - )} - + {item.subject || t('mail.activity_no_subject')} + + {subLine.length > 0 && ( - {item.sender_name || item.sender_email} + {subLine} - - - {formatDate(item.received_at, t)} - + )} ); } diff --git a/apps/rebreak-native/components/mail/MailDistributionChart.tsx b/apps/rebreak-native/components/mail/MailDistributionChart.tsx index 5b587e5..11939a7 100644 --- a/apps/rebreak-native/components/mail/MailDistributionChart.tsx +++ b/apps/rebreak-native/components/mail/MailDistributionChart.tsx @@ -74,15 +74,14 @@ export function MailDistributionChart({ data, hero, totalBlocked, accountCount, const total = data.reduce((s, d) => s + d.count, 0); - // Build donut slices: always cap at 4 visible segments (Top-3 + Sonstige). - // Edge-case: ≤3 → no grouping. Exactly 4 → show all 4 (no grouping). - // 5+ → Top-3 + Sonstige. + // Build donut slices: hard-cap at 3 named entries + "Sonstige" bucket. + // ≤3 accounts → show all (no grouping). 4+ → Top-3 + Sonstige. const slices = useMemo(() => { if (data.length === 0 || total === 0) return []; const sorted = [...data].sort((a, b) => b.count - a.count); - if (sorted.length <= 4) { + if (sorted.length <= MAX_LEGEND_ENTRIES) { return sorted.map((e, i) => ({ label: displayLabel(e), count: e.count, @@ -92,7 +91,7 @@ export function MailDistributionChart({ data, hero, totalBlocked, accountCount, })); } - // 5+ connections: Top-3 + Sonstige bucket + // 4+ connections: Top-3 + Sonstige bucket const top3 = sorted.slice(0, MAX_LEGEND_ENTRIES); const rest = sorted.slice(MAX_LEGEND_ENTRIES); const restCount = rest.reduce((s, e) => s + e.count, 0); @@ -155,16 +154,6 @@ export function MailDistributionChart({ data, hero, totalBlocked, accountCount, > {displayTotal.toLocaleString()} - - {t('mail.stats_account_summary', { count: accountCount ?? data.length })} - {/* Live / Scheduled pill */} diff --git a/apps/rebreak-native/hooks/useMailResults.ts b/apps/rebreak-native/hooks/useMailResults.ts index 8360338..c88766a 100644 --- a/apps/rebreak-native/hooks/useMailResults.ts +++ b/apps/rebreak-native/hooks/useMailResults.ts @@ -4,12 +4,17 @@ import { apiFetch } from "../lib/api"; export type MailBlockedItem = { id: string; subject: string; - sender_email: string; - sender_name: string | null; - received_at: string; - connection_id: string; - connection_title?: string | null; - provider?: string | null; + senderEmail: string; + senderName: string | null; + receivedAt: string; + connectionId: string; + connection?: { + id: string; + email: string; + title: string | null; + provider: string; + providerLabel: string; + } | null; }; export type MailResultsResponse = { diff --git a/backend/imap-idle/index.mjs b/backend/imap-idle/index.mjs index 6058f78..043af0c 100644 --- a/backend/imap-idle/index.mjs +++ b/backend/imap-idle/index.mjs @@ -467,7 +467,18 @@ async function runSession(conn) { log(conn.email, `connected (${conn.imapHost}:${conn.imapPort}, auth=${creds.type})`); attempt = 0; // Reset nach erfolgreicher Verbindung authRetries = 0; // Auth-Retry-Counter ebenfalls reset - await clearConnectionError(conn.id).catch(() => {}); + // Initial-Heartbeat: sofort nach erfolgreichem Connect schreiben damit + // das Frontend "aktiv" zeigt statt bis zum ersten NOOP-Cycle zu warten + // (NOOP-Cycle = alle 2min → worst-case 2min+, gemessene delay 2-9min). + // Gilt für alle auth-Methoden (app_password + oauth2_microsoft) und + // auch für den Re-Connect nach AUTHENTICATIONFAILED-Recovery, da beide + // Paths durch diesen Block laufen. + // last_connect_error wird gleichzeitig geclearet: ein zuvor failed-State + // (z.B. abgelaufener Token) ist nach erfolgreichem Connect behoben. + await Promise.all([ + updateIdleHeartbeat(conn.id).catch(() => {}), + clearConnectionError(conn.id).catch(() => {}), + ]); await imap.getMailboxLock("INBOX");