import React, { useEffect, useRef, useState, useMemo } from 'react'; import { View, Text, TouchableOpacity, Animated, ActivityIndicator } from 'react-native'; import { Image } from 'expo-image'; import { Ionicons } from '@expo/vector-icons'; import { useTranslation } from 'react-i18next'; import { useColors, type ColorScheme } from '../../lib/theme'; import { ConfirmAlert } from '../ConfirmAlert'; import { SuccessAlert } from '../SuccessAlert'; import { useWebContentDomains } from '../../hooks/useWebContentDomains'; import type { CustomDomain, DomainStatus, Tier } from '../../hooks/useCustomDomains'; import { SuggestCuratedSheet } from './SuggestCuratedSheet'; type VipCustomMeta = { status: DomainStatus; vipEvictAt: string | null | undefined }; // ─── Meine Filter (unified web + mail_domain) ───────────────────────────────── type MyFiltersProps = { domains: CustomDomain[]; tier: Tier; totalCount: number; totalLimit: number; globalBlocklistCount: number; onAddPress: () => void; onSubmitDomain: (id: string) => Promise<{ ok: boolean }>; colors: ColorScheme; }; /** * "Meine Filter" — unified Sektion für web + mail_domain Einträge. * * Ein Slot-Pool: totalLimit = Legend 20 / Pro 10 (web+mail zusammen). * Kacheln zeigen Typ-Badge (Web / Mail). Jede Kachel hat einen Freigabe-Button: * der User reicht die Domain an die globale Blocklist ein (Pro = Community-Vote, * Legend = Admin-Review). Bewusst KEIN Entfernen-Button — einmal gesperrt * bleibt gesperrt (Anti-Rückfall-Logik). */ export function MyFiltersList({ domains, tier, totalCount, totalLimit, globalBlocklistCount, onAddPress, onSubmitDomain, colors, }: MyFiltersProps) { const { t } = useTranslation(); const visibleDomains = useMemo( () => domains.filter((d) => d.status !== 'approved'), [domains], ); const atLimit = totalCount >= totalLimit; const fillAnim = useRef(new Animated.Value(0)).current; const ratio = totalLimit > 0 ? Math.min(totalCount / totalLimit, 1) : 0; useEffect(() => { Animated.timing(fillAnim, { toValue: ratio, duration: 380, useNativeDriver: false }).start(); }, [ratio]); const pct = ratio * 100; const barColor = pct >= 90 ? colors.error : pct >= 60 ? colors.warning : colors.brandOrange; const badgeBg = atLimit ? '#fee2e2' : colors.surfaceElevated; const badgeFg = atLimit ? colors.error : colors.textMuted; return ( {t('blocker.my_filters_title')} {t('blocker.count_label', { count: totalCount, max: totalLimit })} {visibleDomains.length === 0 ? ( ) : ( )} {t('blocker.vip_global_hint', { count: globalBlocklistCount })} ); } function MyFiltersEmptyState({ onAddPress, colors }: { onAddPress: () => void; colors: ColorScheme }) { const { t } = useTranslation(); return ( {t('blocker.my_filters_empty')} ); } function FilterTilesGrid({ domains, tier, onSubmit, }: { domains: CustomDomain[]; tier: Tier; onSubmit: (id: string) => Promise<{ ok: boolean }>; }) { return ( {domains.map((d) => ( ))} ); } function FilterTile({ domain, tier, onSubmit, }: { domain: CustomDomain; tier: Tier; onSubmit: (id: string) => Promise<{ ok: boolean }>; }) { const { t } = useTranslation(); const colors = useColors(); const [imgError, setImgError] = useState(false); const [submitting, setSubmitting] = useState(false); const [confirmVisible, setConfirmVisible] = useState(false); const [successVisible, setSuccessVisible] = useState(false); const isMail = domain.type === 'mail_domain'; const stripped = domain.domain.replace(/^www\./, ''); const isLegend = tier.plan === 'legend'; const isResubmit = domain.status === 'rejected'; // Freigabe-Button: nur für noch nicht eingereichte Einträge. mail_display_name // ist eine Substring-Heuristik — kann nicht in die globale Blocklist. const canSubmit = tier.canSubmit && (domain.status === 'active' || domain.status === 'rejected') && domain.type !== 'mail_display_name'; const statusColor: string = (() => { switch (domain.status) { case 'submitted': return colors.warning; case 'rejected': return colors.error; default: return colors.brandOrange; } })(); const badgeLabel: string = (() => { switch (domain.status) { case 'submitted': return t('blocker.domain_badge_pruefung'); case 'rejected': return t('blocker.domain_badge_rejected'); default: return t('blocker.domain_badge_active'); } })(); const btnColor = isResubmit ? colors.error : colors.brandOrange; const confirmTitle = isLegend ? isResubmit ? t('blocker.domain_confirm_legend_resubmit') : t('blocker.domain_confirm_legend_first') : isResubmit ? t('blocker.domain_confirm_community_resubmit') : t('blocker.domain_confirm_community_first'); const confirmMessage = isLegend ? t('blocker.domain_confirm_legend_message', { domain: stripped }) : t('blocker.domain_confirm_community_message', { domain: stripped }); async function handleConfirm() { setConfirmVisible(false); setSubmitting(true); try { const result = await onSubmit(domain.id); if (result.ok) setSuccessVisible(true); } finally { setSubmitting(false); } } return ( <> {/* Type + Status badge row */} {isMail ? t('blocker.type_mail') : t('blocker.type_web')} {badgeLabel} {/* Icon + label */} {isMail ? ( ) : !imgError ? ( setImgError(true)} /> ) : ( {stripped.slice(0, 2).toUpperCase()} )} {stripped} {/* Bottom slot — Freigabe / Erneut / in Prüfung. Immer 26px hoch, damit alle Kacheln gleich hoch bleiben. */} {domain.status === 'submitted' ? ( {isLegend ? t('blocker.domain_btn_rebreak_prueft') : t('blocker.domain_btn_in_abstimmung')} ) : canSubmit ? ( setConfirmVisible(true)} disabled={submitting} activeOpacity={0.65} style={{ flex: 1, borderRadius: 6, borderWidth: 1, borderColor: btnColor, alignItems: 'center', justifyContent: 'center', }} > {submitting ? ( ) : ( {isResubmit ? t('blocker.domain_btn_erneut') : t('blocker.domain_btn_freigeben')} )} ) : null} setConfirmVisible(false)} /> setSuccessVisible(false)} /> ); } // ─── VIP-Liste (collapsible, Zweitschutz) ──────────────────────────────────── type VipListProps = { domains: CustomDomain[]; open: boolean; onToggle: () => void; colors: ColorScheme; }; export function VipDomainList({ domains, open, onToggle, colors }: VipListProps) { const { t } = useTranslation(); const { domains: vipList, loading, refetch } = useWebContentDomains(); const [suggestSheetVisible, setSuggestSheetVisible] = useState(false); const webCustoms = useMemo( () => domains.filter((d) => (d.type === 'web' || !d.type) && d.status !== 'rejected'), [domains], ); const customStatusMap = useMemo(() => { const m = new Map(); for (const d of webCustoms) { m.set(d.domain.replace(/^www\./, ''), { status: d.status, vipEvictAt: d.vipEvictAt }); } return m; }, [webCustoms]); const domainsSig = useMemo( () => domains.map((d) => `${d.id}:${d.status}`).join('|'), [domains], ); const firstRunRef = useRef(true); useEffect(() => { if (firstRunRef.current) { firstRunRef.current = false; return; } refetch(); }, [domainsSig, refetch]); const list = vipList ?? [...customStatusMap.keys()]; const now = Date.now(); const customDomains = list .filter((d) => customStatusMap.has(d)) .sort((a, b) => { const evictA = customStatusMap.get(a)?.vipEvictAt; const evictB = customStatusMap.get(b)?.vipEvictAt; const aIsPending = evictA ? new Date(evictA).getTime() > now : false; const bIsPending = evictB ? new Date(evictB).getTime() > now : false; if (aIsPending && !bIsPending) return -1; if (!aIsPending && bIsPending) return 1; return 0; }); const curatedDomains = list.filter((d) => !customStatusMap.has(d)); function getMeta(d: string): VipCustomMeta { return customStatusMap.get(d) ?? { status: 'active', vipEvictAt: null }; } return ( {t('blocker.vip_layer2_title')} {open && ( {t('blocker.vip_layer2_desc')} {loading && vipList === null ? ( ) : ( <> {customDomains.length > 0 && ( {customDomains.map((d) => { const meta = getMeta(d); return ( ); })} )} {curatedDomains.length > 0 && ( setSuggestSheetVisible(true)} > {curatedDomains.map((d) => ( ))} )} {list.length === 0 && ( {t('blocker.vip_layer2_count', { count: 0 })} )} )} )} setSuggestSheetVisible(false)} /> ); } function VipSubSection({ title, count, colors, children, onSuggest, }: { title: string; count: string; colors: ColorScheme; children: React.ReactNode; onSuggest?: () => void; }) { const { t } = useTranslation(); return ( {title} {count} {onSuggest && ( {t('blocker.suggest_curated_link')} )} {children} ); } function VipCustomTile({ domain, status, vipEvictAt, colors, }: { domain: string; status: DomainStatus; vipEvictAt?: string | null; colors: ColorScheme; }) { const { t } = useTranslation(); const [imgError, setImgError] = useState(false); const stripped = domain.replace(/^www\./, ''); const evictBadgeHours: number | null = (() => { if (!vipEvictAt) return null; const ms = new Date(vipEvictAt).getTime() - Date.now(); if (ms <= 0) return null; return Math.ceil(ms / (1000 * 60 * 60)); })(); const isEvictPending = evictBadgeHours !== null; const statusColor: string = (() => { if (status === 'submitted') return colors.warning; if (isEvictPending) return '#f97316'; return '#22c55e'; })(); const badgeLabel: string = (() => { switch (status) { case 'submitted': return t('blocker.domain_badge_pruefung'); default: return t('blocker.domain_badge_active'); } })(); return ( {badgeLabel} {!imgError ? ( setImgError(true)} /> ) : ( {stripped.slice(0, 2).toUpperCase()} )} {stripped} {evictBadgeHours !== null && ( {t('blocker.vip_evict_badge', { hours: evictBadgeHours })} )} ); } function VipCuratedTile({ domain, colors }: { domain: string; colors: ColorScheme }) { const [imgError, setImgError] = useState(false); const stripped = domain.replace(/^www\./, ''); return ( {!imgError ? ( setImgError(true)} /> ) : ( {stripped.slice(0, 2).toUpperCase()} )} {stripped} ); }