chahinebrini 6bbf9e4cfd fix(native/mail): kürzlich-blockiert uses createdAt, not the original receive date
User saw entries like "vor 61d · Outlook" under the "Kürzlich
blockiert · In den letzten 24h" header. createdAt (when the daemon
wrote the mail_blocked row) is always inside the 24h retention window
because deleteOldMailBlocked sweeps everything older than that on
every fetch — but the row preserves the original receivedAt header
from the email, which for old Casino mails the daemon only just got
around to scanning can be weeks or months ago.

Switched the time-label in MailActivityLog to format createdAt
instead. The MailBlockedItem type now carries createdAt explicitly
(the backend has been returning it all along, the FE type just hadn't
acknowledged it). receivedAt stays in the shape for any future
"received vs blocked" comparison view but isn't used in the recent-
activity list anymore.
2026-05-16 05:26:52 +02:00

289 lines
8.4 KiB
TypeScript

import { useState } from 'react';
import {
LayoutAnimation,
Platform,
ScrollView,
TouchableOpacity,
Text,
UIManager,
View,
} from 'react-native';
import { Ionicons } from '@expo/vector-icons';
import { useTranslation } from 'react-i18next';
import { useMailResults, type MailBlockedItem } from '../../hooks/useMailResults';
import { useColors } from '../../lib/theme';
if (Platform.OS === 'android' && UIManager.setLayoutAnimationEnabledExperimental) {
UIManager.setLayoutAnimationEnabledExperimental(true);
}
type Props = {
expanded: boolean;
onToggle: () => void;
providers?: string[];
};
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 `vor ${hours}h`;
return `vor ${Math.floor(hours / 24)}d`;
}
function domainFromEmail(email: string): string {
return email.split('@')[1] ?? email;
}
function providerDisplayName(provider: string): string {
const map: Record<string, string> = {
gmail: 'Gmail',
icloud: 'iCloud',
outlook: 'Outlook',
yahoo: 'Yahoo',
gmx: 'GMX',
other: 'Andere',
};
return map[provider.toLowerCase()] ?? provider;
}
export function MailActivityLog({ expanded, onToggle, providers = [] }: Props) {
const { t } = useTranslation();
const colors = useColors();
function handleToggle() {
LayoutAnimation.configureNext(LayoutAnimation.Presets.easeInEaseOut);
onToggle();
}
return (
<View
style={{
backgroundColor: colors.surface,
borderRadius: 16,
borderWidth: 1,
borderColor: colors.border,
overflow: 'hidden',
}}
>
<TouchableOpacity onPress={handleToggle} activeOpacity={0.85}>
<View
style={{
flexDirection: 'row',
alignItems: 'center',
paddingHorizontal: 14,
paddingVertical: 14,
}}
>
<View
style={{
width: 32,
height: 32,
borderRadius: 8,
backgroundColor: '#fef2f2',
alignItems: 'center',
justifyContent: 'center',
marginRight: 12,
}}
>
<Ionicons name="trash" size={15} color="#dc2626" />
</View>
<View style={{ flex: 1, minWidth: 0, marginRight: 8 }}>
<Text
style={{ fontSize: 14, fontFamily: 'Nunito_700Bold', color: colors.text }}
numberOfLines={1}
>
{t('mail.activity_log_title')}
</Text>
<Text
style={{
fontSize: 11,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
marginTop: 2,
}}
numberOfLines={1}
>
{t('mail.activity_log_subtitle')}
</Text>
</View>
<Ionicons
name={expanded ? 'chevron-up' : 'chevron-down'}
size={18}
color={colors.textMuted}
/>
</View>
</TouchableOpacity>
{expanded && (
<View style={{ borderTopWidth: 1, borderTopColor: colors.border }}>
<MailActivityLogBody providers={providers} colors={colors} />
</View>
)}
</View>
);
}
/**
* Only the expandable body — used standalone by MoreInfosSection as nested collapsible.
*/
export function MailActivityLogBody({
providers = [],
colors,
}: {
providers?: string[];
colors: ReturnType<typeof useColors>;
}) {
const { t } = useTranslation();
const [activeProvider, setActiveProvider] = useState('all');
const { results, total, loading, refresh } = useMailResults(true, activeProvider);
const filterOptions = ['all', ...providers];
return (
<View style={{ borderTopWidth: 1, borderTopColor: colors.border }}>
{/* Provider filter chips */}
{filterOptions.length > 1 && (
<ScrollView
horizontal
showsHorizontalScrollIndicator={false}
contentContainerStyle={{ paddingHorizontal: 14, paddingVertical: 10, gap: 6 }}
>
{filterOptions.map((p) => {
const active = activeProvider === p;
return (
<TouchableOpacity
key={p}
activeOpacity={0.7}
onPress={() => setActiveProvider(p)}
style={{
paddingHorizontal: 12,
paddingVertical: 5,
borderRadius: 999,
backgroundColor: active ? '#007AFF' : colors.surfaceElevated,
borderWidth: active ? 0 : 1,
borderColor: colors.border,
}}
>
<Text
style={{
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: active ? '#fff' : colors.textMuted,
}}
>
{p === 'all' ? t('mail.filter.all') : providerDisplayName(p)}
</Text>
</TouchableOpacity>
);
})}
</ScrollView>
)}
{loading && results.length === 0 ? (
<View style={{ padding: 20, alignItems: 'center' }}>
<Text style={{ fontSize: 12, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{t('mail.loading')}
</Text>
</View>
) : results.length === 0 ? (
<View style={{ padding: 24, alignItems: 'center' }}>
<Ionicons name="checkmark-circle-outline" size={28} color={colors.success} />
<Text
style={{
fontSize: 12,
fontFamily: 'Nunito_600SemiBold',
color: colors.textMuted,
marginTop: 6,
}}
>
{t('mail.activity_log_empty')}
</Text>
</View>
) : (
<>
{results.slice(0, 10).map((item) => (
<ActivityItem key={item.id} item={item} t={t} colors={colors} />
))}
<View
style={{
flexDirection: 'row',
alignItems: 'center',
justifyContent: 'space-between',
paddingHorizontal: 14,
paddingVertical: 10,
borderTopWidth: 1,
borderTopColor: colors.border,
}}
>
<Text style={{ fontSize: 11, fontFamily: 'Nunito_400Regular', color: colors.textMuted }}>
{total > 10
? t('mail.activity_log_more', { count: total - 10 })
: t('mail.activity_log_count', { count: total })}
</Text>
<TouchableOpacity activeOpacity={0.7} onPress={refresh} hitSlop={8}>
<Ionicons name="refresh" size={14} color={colors.textMuted} />
</TouchableOpacity>
</View>
</>
)}
</View>
);
}
function ActivityItem({
item,
t,
colors,
}: {
item: MailBlockedItem;
t: (k: string, opts?: any) => string;
colors: ReturnType<typeof useColors>;
}) {
const providerLabel = item.connection?.providerLabel ?? (
item.senderEmail ? domainFromEmail(item.senderEmail) : null
);
// createdAt = wann WIR die Mail geblockt haben. receivedAt = wann der
// Sender sie ursprünglich rausgeschickt hat — bei alten Casino-Mails die
// wir gerade erst gescant haben, ist das oft Wochen/Monate her und
// verwirrt den User in der "Kürzlich blockiert"-Liste.
const timeLabel = formatDate(item.createdAt, t);
const subLine = [timeLabel, providerLabel].filter(Boolean).join(' · ');
return (
<View
style={{
paddingHorizontal: 14,
paddingVertical: 10,
borderTopWidth: 1,
borderTopColor: colors.border,
}}
>
<Text
style={{ fontSize: 13, fontFamily: 'Nunito_600SemiBold', color: colors.text }}
numberOfLines={1}
>
{item.subject || t('mail.activity_no_subject')}
</Text>
{subLine.length > 0 && (
<Text
style={{
fontSize: 11,
fontFamily: 'Nunito_400Regular',
color: colors.textMuted,
marginTop: 2,
}}
numberOfLines={1}
>
{subLine}
</Text>
)}
</View>
);
}