diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx index 559fa84..851d01e 100644 --- a/apps/rebreak-native/app/(app)/blocker.tsx +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -1,9 +1,10 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { ScrollView, Text, View, Alert, ActivityIndicator } from 'react-native'; +import { ScrollView, Text, View, Alert, ActivityIndicator, TouchableOpacity } from 'react-native'; import { useRouter } from 'expo-router'; import { useBottomTabBarHeight } from 'react-native-bottom-tabs'; import { useTranslation } from 'react-i18next'; import { Ionicons } from '@expo/vector-icons'; +import type { CountsByType, LimitsByType } from '../../hooks/useCustomDomains'; import { AppHeader } from '../../components/AppHeader'; import { LayerSwitchCard } from '../../components/blocker/LayerSwitchCard'; import { ProtectionLockedCard } from '../../components/blocker/ProtectionLockedCard'; @@ -42,6 +43,8 @@ export default function BlockerScreen() { const { domains, tier, + countsByType, + limits, addDomain, submitDomain, refresh: refreshDomains, @@ -62,6 +65,9 @@ export default function BlockerScreen() { }, [refreshDomains, syncBlocklist, refresh]); useDomainSubmissionRealtime(onDomainChange, true); + // Tab-State + const [activeTab, setActiveTab] = useState<'web' | 'mail'>('web'); + // Sheet-States const [addSheetOpen, setAddSheetOpen] = useState(false); const [detailsOpen, setDetailsOpen] = useState(false); @@ -367,11 +373,20 @@ export default function BlockerScreen() { )} + {/* Top-Tabs: Seiten / Mails */} + + {/* Domain Grid mit inline + Button neben SlotPill */} setAddSheetOpen(true)} onSubmit={submitDomain} onUpgradePro={() => Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))} @@ -383,6 +398,7 @@ export default function BlockerScreen() { { setAddSheetOpen(false); refreshDomains(); @@ -417,3 +433,88 @@ export default function BlockerScreen() { ); } + +// ─── BlockerTabBar ──────────────────────────────────────────────────────────── + +function BlockerTabBar({ + activeTab, + onTabChange, + countsByType, + limits, +}: { + activeTab: 'web' | 'mail'; + onTabChange: (tab: 'web' | 'mail') => void; + countsByType: CountsByType; + limits: LimitsByType; +}) { + const { t } = useTranslation(); + const colors = useColors(); + + const tabs: { key: 'web' | 'mail'; label: string; count: number; max: number }[] = [ + { key: 'web', label: t('blocker.tabs_web'), count: countsByType.web, max: limits.web }, + { key: 'mail', label: t('blocker.tabs_mail'), count: countsByType.mail, max: limits.mail }, + ]; + + return ( + + {tabs.map((tab) => { + const isActive = activeTab === tab.key; + const atMax = tab.count >= tab.max; + const badgeColor = atMax ? colors.error : colors.textMuted; + + return ( + onTabChange(tab.key)} + activeOpacity={0.7} + style={{ + flex: 1, + alignItems: 'center', + paddingBottom: 10, + paddingTop: 4, + borderBottomWidth: 2, + borderBottomColor: isActive ? colors.brandOrange : 'transparent', + }} + > + + + {tab.label} + + + + {t('blocker.count_label', { count: tab.count, max: tab.max })} + + + + + ); + })} + + ); +} diff --git a/apps/rebreak-native/components/blocker/DomainGrid.tsx b/apps/rebreak-native/components/blocker/DomainGrid.tsx index 2b8a660..b6d3797 100644 --- a/apps/rebreak-native/components/blocker/DomainGrid.tsx +++ b/apps/rebreak-native/components/blocker/DomainGrid.tsx @@ -2,7 +2,6 @@ import { useState, useMemo } from 'react'; import { View, Text, - Pressable, TouchableOpacity, Image, ActivityIndicator, @@ -50,6 +49,7 @@ function timeSinceSubmit(input?: string | Date): string { type Props = { domains: CustomDomain[]; tier: Tier; + activeTab: 'web' | 'mail'; onAdd?: () => void; onSubmit?: (id: string) => Promise<{ ok: boolean }>; onRemove?: (id: string) => Promise<{ ok: boolean }>; @@ -65,14 +65,19 @@ const STATUS_PRIORITY: Record = { approved: 99, }; -export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Props) { +export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgradePro }: Props) { const { t } = useTranslation(); const colors = useColors(); - // Slot-relevante Domains (alles außer approved). Sortiert nach Status-Priority, - // innerhalb gleicher Priority dann newest-first by addedAt. + // Filter by tab, then by status (exclude approved). Sort by status-priority, then newest-first. const visible = useMemo(() => { return domains - .filter((d) => d.status !== 'approved') + .filter((d) => { + if (d.status === 'approved') return false; + if (activeTab === 'mail') { + return d.type === 'mail_domain' || d.type === 'mail_display_name'; + } + return d.type === 'web' || !d.type; + }) .slice() .sort((a, b) => { const pa = STATUS_PRIORITY[a.status] ?? 99; @@ -82,7 +87,7 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0; return tb - ta; }); - }, [domains]); + }, [domains, activeTab]); return ( @@ -94,11 +99,11 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro {onAdd && ( - - + )} @@ -179,7 +183,11 @@ export function DomainGrid({ domains, tier, onAdd, onSubmit, onUpgradePro }: Pro alignItems: 'center', }} > - + - {t('blocker.domain_empty')} + {activeTab === 'mail' ? t('blocker.empty_mail') : t('blocker.empty_web')} ) : ( @@ -341,9 +349,10 @@ function DomainTile({ } } + const isMailDisplayName = domain.type === 'mail_display_name'; const isFreeAndUsed = tier.plan === 'free' && domain.status !== 'active'; - const showSubmit = tier.canSubmit && domain.status === 'active'; - const showResubmit = tier.canSubmit && domain.status === 'rejected'; + const showSubmit = tier.canSubmit && domain.status === 'active' && !isMailDisplayName; + const showResubmit = tier.canSubmit && domain.status === 'rejected' && !isMailDisplayName; const showInPruefungBtn = domain.status === 'submitted'; return ( @@ -461,9 +470,10 @@ function DomainTile({ )} {showSubmit && ( - )} - + )} {showResubmit && ( - )} - + )} diff --git a/apps/rebreak-native/hooks/useCustomDomains.ts b/apps/rebreak-native/hooks/useCustomDomains.ts index 918f7d2..c1661c3 100644 --- a/apps/rebreak-native/hooks/useCustomDomains.ts +++ b/apps/rebreak-native/hooks/useCustomDomains.ts @@ -42,9 +42,21 @@ function deriveTier(plan: Plan, domains: CustomDomain[]): Tier { }; } +export type CountsByType = { + web: number; + mail: number; +}; + +export type LimitsByType = { + web: number; + mail: number; +}; + export type UseCustomDomainsReturn = { domains: CustomDomain[]; tier: Tier; + countsByType: CountsByType; + limits: LimitsByType; loading: boolean; error: string | null; refresh: () => Promise; @@ -165,9 +177,24 @@ export function useCustomDomains(plan: Plan): UseCustomDomainsReturn { const tier = deriveTier(plan, domains); + const countsByType: CountsByType = { + web: domains.filter( + (d) => d.status !== 'approved' && (d.type === 'web' || !d.type), + ).length, + mail: domains.filter( + (d) => d.status !== 'approved' && (d.type === 'mail_domain' || d.type === 'mail_display_name'), + ).length, + }; + + const webLimit = plan === 'legend' ? 8 : 4; + const mailLimit = plan === 'legend' ? 2 : 1; + const limits: LimitsByType = { web: webLimit, mail: mailLimit }; + return { domains, tier, + countsByType, + limits, loading, error, refresh: fetchDomains, diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index c60a72d..1786eea 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -325,7 +325,14 @@ "add_mail_label": "E-Mail-Absender oder Display-Name", "add_mail_placeholder": "z.B. only4-subscribers.com oder EXTRASPIN", "add_mail_help": "Mail-Adresse, Domain oder Display-Name. Wir blockieren alle Mails die diesem Muster entsprechen.", - "add_mail_invalid": "Bitte ein Muster eingeben." + "add_mail_invalid": "Bitte ein Muster eingeben.", + "tabs_web": "Seiten", + "tabs_mail": "Mails", + "count_label": "%{count}/%{max}", + "error_web_limit_reached": "Du hast alle Domain-Slots aufgebraucht. Entferne eine Domain oder upgrade auf Pro/Legend.", + "error_mail_limit_reached": "Du hast alle Mail-Slots aufgebraucht. Entferne ein Mail-Pattern oder upgrade auf Pro/Legend.", + "empty_web": "Noch keine eigenen Domains.\nTippe + um eine hinzuzufügen.", + "empty_mail": "Noch keine E-Mail-Patterns. Tippe + um eine Adresse oder einen Display-Namen (z.B. EXTRASPIN) zu blockieren." }, "mail": { "title": "Mail-Schutz", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index dac6f71..8b90e39 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -325,7 +325,14 @@ "add_mail_label": "Email sender or display name", "add_mail_placeholder": "e.g. only4-subscribers.com or EXTRASPIN", "add_mail_help": "Email address, domain or display name. We block all emails matching this pattern.", - "add_mail_invalid": "Please enter a pattern." + "add_mail_invalid": "Please enter a pattern.", + "tabs_web": "Websites", + "tabs_mail": "Emails", + "count_label": "%{count}/%{max}", + "error_web_limit_reached": "You've used all your domain slots. Remove a domain or upgrade to Pro/Legend.", + "error_mail_limit_reached": "You've used all your email slots. Remove an email pattern or upgrade to Pro/Legend.", + "empty_web": "No custom domains yet.\nTap + to add one.", + "empty_mail": "No email patterns yet. Tap + to block an address or display name (e.g. EXTRASPIN)." }, "mail": { "title": "Mail Shield", diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json index 2346fe8..a0cc9ab 100644 --- a/apps/rebreak-native/locales/fr.json +++ b/apps/rebreak-native/locales/fr.json @@ -325,7 +325,14 @@ "add_mail_label": "Expéditeur ou nom affiché", "add_mail_placeholder": "ex. only4-subscribers.com ou EXTRASPIN", "add_mail_help": "Adresse e-mail, domaine ou nom affiché. Nous bloquons tous les mails correspondant à ce modèle.", - "add_mail_invalid": "Veuillez saisir un modèle." + "add_mail_invalid": "Veuillez saisir un modèle.", + "tabs_web": "Sites", + "tabs_mail": "E-mails", + "count_label": "%{count}/%{max}", + "error_web_limit_reached": "Vous avez utilisé tous vos emplacements de domaines. Supprimez un domaine ou passez à Pro/Legend.", + "error_mail_limit_reached": "Vous avez utilisé tous vos emplacements e-mail. Supprimez un modèle ou passez à Pro/Legend.", + "empty_web": "Aucun domaine personnalisé.\nAppuyez sur + pour en ajouter un.", + "empty_mail": "Aucun filtre e-mail. Appuyez sur + pour bloquer une adresse ou un nom affiché (ex. EXTRASPIN)." }, "mail": { "title": "Protection Mail",