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.
289 lines
8.4 KiB
TypeScript
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>
|
|
);
|
|
}
|