From f4da81f551529228d3b445ddc51ac4a80274d176 Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sat, 16 May 2026 02:19:27 +0200 Subject: [PATCH] feat(native/blocker): two collapsible sections + new AddDomainSheet layout MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The Seiten/Mails top-tabs added in 5c6fa3d are gone. Per the user's revised vision, web-domains and mail-patterns live side by side as two collapsible s with their own header, slot pill, progress bar, and add-button — closer to the original Eigene-Domains affordance plus a sibling Eigene-Mails section. Both default open; chevron-up/down per the existing icon convention. AddDomainSheet was rewritten from scratch to fix the layout-bug visible in the screenshot — SheetFieldStack's two-ScrollView intro/ fields split was wrong for a single-input use case and was rendering the chip at the bottom of the scroll area with a huge gap under the TypePicker. The new sheet is a plain ScrollView with TypePicker, label, TextInput, help-card, preview-card, warning-card, confirm-row, and the Cancel + Hinzufügen buttons stacked top-to-bottom with `gap: 12`. No Pressable anywhere — TouchableOpacity only, per the hard rule. DomainGrid is now a pure tile renderer: the header / slot pill / add affordance live on the section component above it. Its `kind` prop (renamed from `activeTab`) drives the type filter — for v1.0, mail means strictly `mail_domain` (display-name is gone). i18n: new keys section_domains / section_mails / add_sheet_cta. mail- related copy (label, placeholder, help, empty) had every "Display-Name" mention stripped so the user can't read about an option that doesn't ship. Progressbar inline in DomainSection with the same Animated.timing pattern DeviceProgressBar uses, with a 3-step color threshold (green / brandOrange / error) keyed on the bucket fill ratio. --- apps/rebreak-native/app/(app)/blocker.tsx | 277 +++++++++++------- .../components/blocker/AddDomainSheet.tsx | 204 +++++++------ .../components/blocker/DomainGrid.tsx | 130 +------- apps/rebreak-native/hooks/useCustomDomains.ts | 2 +- apps/rebreak-native/locales/de.json | 11 +- apps/rebreak-native/locales/en.json | 11 +- apps/rebreak-native/locales/fr.json | 11 +- 7 files changed, 323 insertions(+), 323 deletions(-) diff --git a/apps/rebreak-native/app/(app)/blocker.tsx b/apps/rebreak-native/app/(app)/blocker.tsx index 851d01e..9cab1a1 100644 --- a/apps/rebreak-native/app/(app)/blocker.tsx +++ b/apps/rebreak-native/app/(app)/blocker.tsx @@ -1,10 +1,9 @@ import { useCallback, useEffect, useRef, useState } from 'react'; -import { ScrollView, Text, View, Alert, ActivityIndicator, TouchableOpacity } from 'react-native'; +import { Animated, 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'; @@ -52,9 +51,6 @@ export default function BlockerScreen() { const { sync: syncBlocklist } = useBlocklistSync(); // Realtime: Domain-Submission-Status (approved/rejected/in_review) live patchen. - // Bei domain_rejected wird die Row backend-seitig hard-deleted → refetch - // entfernt sie aus der Liste. Zusätzlich blocklist.bin neu syncen damit - // die lokale Hash-Liste nicht aus dem Tritt gerät. const onDomainChange = useCallback(async () => { await refreshDomains(); if (urlFilterActiveRef.current) { @@ -65,28 +61,26 @@ export default function BlockerScreen() { }, [refreshDomains, syncBlocklist, refresh]); useDomainSubmissionRealtime(onDomainChange, true); - // Tab-State - const [activeTab, setActiveTab] = useState<'web' | 'mail'>('web'); + // Section collapse state — beide Sections starten offen + const [webOpen, setWebOpen] = useState(true); + const [mailOpen, setMailOpen] = useState(true); - // Sheet-States + // AddSheet state: tracks which section opened it const [addSheetOpen, setAddSheetOpen] = useState(false); + const [addSheetKind, setAddSheetKind] = useState<'web' | 'mail'>('web'); + const [detailsOpen, setDetailsOpen] = useState(false); const [explainerOpen, setExplainerOpen] = useState(false); - // Layer-Status (auf iOS): urlFilter + familyControls. - // AppDeletionLock=true bedeutet "locked in" → keine Switches mehr, nur Cooldown-Pfad. const urlFilterActive = state?.layers.urlFilter === true; const familyControlsActive = state?.layers.familyControls === true; const appDeletionLockActive = (state?.layers.appDeletionLock ?? familyControlsActive) === true; const lockedIn = appDeletionLockActive; - // Ref damit onDomainChange nicht neu rendert bei jedem urlFilter-Toggle const urlFilterActiveRef = useRef(urlFilterActive); useEffect(() => { urlFilterActiveRef.current = urlFilterActive; }, [urlFilterActive]); - // Auto-Sync wenn URL-Filter beim Page-Mount/-Resume schon aktiv ist und - // blocklist.bin leer/stale sein könnte. Dedupe via Ref damit wir nicht - // bei jedem Re-Render neu syncen. + // Auto-Sync wenn URL-Filter beim Page-Mount/-Resume schon aktiv ist. const syncedOnceRef = useRef(false); useEffect(() => { if (!urlFilterActive) return; @@ -94,7 +88,7 @@ export default function BlockerScreen() { syncedOnceRef.current = true; syncBlocklist().then((res) => { console.log('[blocker] auto-sync on mount:', res); - if (res.ok) refresh(); // Stats-Card neu rendern mit aktuellem Count + if (res.ok) refresh(); }); }, [urlFilterActive, syncBlocklist, refresh]); @@ -114,12 +108,9 @@ export default function BlockerScreen() { ], ); } else { - // Filter ist aktiv aber blocklist.bin ist initial leer — sofort syncen! - // Sonst zeigt iOS "Läuft" aber blockt nichts. const sync = await syncBlocklist(); console.log('[blocker] post-activate sync:', sync); if (sync.ok) { - // Stats-Card neu rendern mit dem frisch geschriebenen Count await refresh(); } else { Alert.alert( @@ -140,10 +131,8 @@ export default function BlockerScreen() { try { const result = await activateFamilyControls(); console.log('[blocker] activateFamilyControls:', result); - // `accessibility_pending` = die a11y-Berechtigung fehlt noch und wir haben - // grad die System-Settings geöffnet → das IST das Feedback. Kein Fehler- - // Modal (sonst Modal-Loop bei jedem Tap). a11y wird nur beim ersten - // Einrichten geholt; danach ist das hier ein 1-Tap-Arm ohne Dialog. + // `accessibility_pending` = a11y-Berechtigung fehlt noch → System-Settings wurden + // geöffnet. Kein Fehler-Modal (sonst Modal-Loop bei jedem Tap). if (!result.enabled && result.error !== 'accessibility_pending') { Alert.alert( t('blocker.activate_app_lock_failed_title'), @@ -199,8 +188,6 @@ export default function BlockerScreen() { } if (bypassAlertShownRef.current) return; bypassAlertShownRef.current = true; - // Schutz-Filter ist aus, sollte aber an sein → Reaktivierung setzt NUR den - // VPN/Filter wieder (kein a11y-Prompt — das passiert nur beim ersten Mal). Alert.alert( t('blocker.protection_off_title'), t('blocker.protection_off_message'), @@ -211,6 +198,11 @@ export default function BlockerScreen() { ); }, [state?.phase, t]); + function openAddSheet(kind: 'web' | 'mail') { + setAddSheetKind(kind); + setAddSheetOpen(true); + } + // ─── Render ────────────────────────────────────────────────────────── return ( @@ -226,7 +218,7 @@ export default function BlockerScreen() { ) : ( - // FC nicht aktiv → User kann pro Layer einzeln togglen )} - {/* CooldownBanner — nur wenn Cooldown läuft */} + {/* CooldownBanner */} {state.cooldown.active && ( )} - {/* Über-Limit: Custom-Domain-Banner */} + {/* Über-Limit Banner */} {tier.atLimit && tier.usedSlots > tier.domainLimit && ( )} - {/* Top-Tabs: Seiten / Mails */} - - - {/* Domain Grid mit inline + Button neben SlotPill */} - + {/* Section 1: Eigene Domains */} + setWebOpen((v) => !v)} + onAdd={() => openAddSheet('web')} + atLimit={countsByType.web >= limits.web} + > setAddSheetOpen(true)} + kind="web" onSubmit={submitDomain} onUpgradePro={() => Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))} /> - + + + {/* Section 2: Eigene Mails */} + setMailOpen((v) => !v)} + onAdd={() => openAddSheet('mail')} + atLimit={countsByType.mail >= limits.mail} + > + Alert.alert(t('blocker.upgrade_alert_title'), t('blocker.upgrade_alert_desc'))} + /> + {/* Sheets */} { setAddSheetOpen(false); refreshDomains(); @@ -406,9 +415,8 @@ export default function BlockerScreen() { onAdd={async (pattern, kind) => { const result = await addDomain(pattern, kind); if (result.ok) { - // Neue Custom-Domain → Filter muss aktualisierten Hash-Set kriegen const sync = await syncBlocklist(); - if (sync.ok) refresh(); // Stats-Card mit neuem Count refreshen + if (sync.ok) refresh(); } return result; }} @@ -434,87 +442,152 @@ export default function BlockerScreen() { ); } -// ─── BlockerTabBar ──────────────────────────────────────────────────────────── +// ─── DomainSection ──────────────────────────────────────────────────────────── -function BlockerTabBar({ - activeTab, - onTabChange, - countsByType, - limits, +function DomainSection({ + title, + count, + max, + open, + onToggle, + onAdd, + atLimit, + children, }: { - activeTab: 'web' | 'mail'; - onTabChange: (tab: 'web' | 'mail') => void; - countsByType: CountsByType; - limits: LimitsByType; + title: string; + count: number; + max: number; + open: boolean; + onToggle: () => void; + onAdd: () => void; + atLimit: boolean; + children: React.ReactNode; }) { 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 }, - ]; + // Animated progress bar + const fillAnim = useRef(new Animated.Value(0)).current; + const ratio = max > 0 ? Math.min(count / max, 1) : 0; + + useEffect(() => { + Animated.timing(fillAnim, { + toValue: ratio, + duration: 380, + useNativeDriver: false, + }).start(); + }, [ratio]); + + const pct = ratio * 100; + const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a'; + + const badgeBg = atLimit ? '#fee2e2' : colors.surfaceElevated; + const badgeFg = atLimit ? '#dc2626' : colors.textMuted; return ( - {tabs.map((tab) => { - const isActive = activeTab === tab.key; - const atMax = tab.count >= tab.max; - const badgeColor = atMax ? colors.error : colors.textMuted; + {/* Section Header */} + + + {title} + + + + {t('blocker.count_label', { count, max })} + + + + - return ( - onTabChange(tab.key)} - activeOpacity={0.7} + {open && ( + + {/* Progressbar */} + - + + + + {/* Add-Button */} + + + - {tab.label} + {t('blocker.add_domain')} - - - {t('blocker.count_label', { count: tab.count, max: tab.max })} - - - ); - })} + + {/* Grid */} + {children} + + )} ); } diff --git a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx index 9e8c2ad..ec3489b 100644 --- a/apps/rebreak-native/components/blocker/AddDomainSheet.tsx +++ b/apps/rebreak-native/components/blocker/AddDomainSheet.tsx @@ -2,7 +2,9 @@ import { useState, useEffect } from 'react'; import { ActivityIndicator, Image, + ScrollView, Text, + TextInput, TouchableOpacity, View, } from 'react-native'; @@ -15,7 +17,6 @@ import { } from '../../hooks/useCustomDomains'; import { useColors, type ColorScheme } from '../../lib/theme'; import { FormSheet } from '../FormSheet'; -import { SheetFieldStack } from '../SheetFieldStack'; type InputKind = 'web' | 'mail'; @@ -35,7 +36,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P const [confirmPermanent, setConfirmPermanent] = useState(false); const [adding, setAdding] = useState(false); const [error, setError] = useState(null); - const [fieldsDone, setFieldsDone] = useState(false); useEffect(() => { if (visible) setKind(initialType ?? 'web'); @@ -44,17 +44,13 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P const normalizedWeb = kind === 'web' ? normalizeDomain(input) : ''; // For mail input: if the user typed a full address (local@domain.tld), strip - // the local-part and keep only the domain — casino affiliates rotate the - // local-part rapidly (communications@, newsletter@, info@, …) while the - // sender-domain stays stable. Blocking on the domain is more durable. - // A bare token without "@" stays as-is (will be matched as display-name on - // the backend). + // the local-part and keep only the domain. A bare domain without "@" stays as-is. const mailPattern = (() => { if (kind !== 'mail') return ''; const raw = input.trim(); if (!raw) return ''; const atIdx = raw.lastIndexOf('@'); - if (atIdx === -1) return raw; + if (atIdx === -1) return raw.toLowerCase(); return raw.slice(atIdx + 1).trim().toLowerCase(); })(); @@ -62,7 +58,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P setInput(''); setConfirmPermanent(false); setError(null); - setFieldsDone(false); onClose(); } @@ -71,7 +66,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P setKind(next); setInput(''); setError(null); - setFieldsDone(false); } function isInputValid(): boolean { @@ -83,7 +77,7 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P if (!isInputValid() || !confirmPermanent || adding) return; setAdding(true); setError(null); - const pattern = kind === 'web' ? input : mailPattern; + const pattern = kind === 'web' ? normalizeDomain(input) : mailPattern; const result = await onAdd(pattern, kind); setAdding(false); if (result.ok) { @@ -118,39 +112,56 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P ? t('blocker.add_web_help') : t('blocker.add_mail_help'); - const validateField = kind === 'web' - ? (v: string) => isValidDomain(v) ? undefined : t('blocker.add_sheet_invalid') - : (v: string) => v.trim().length > 0 ? undefined : t('blocker.add_mail_invalid'); + const canSubmit = isInputValid() && confirmPermanent && !adding; return ( - - } - fields={[ - { - key: 'pattern', - label: inputLabel, - placeholder: inputPlaceholder, - value: input, - onChangeText: (v) => { setInput(v); setError(null); }, - normalize: kind === 'web' ? normalizeDomain : undefined, - keyboardType: kind === 'web' ? 'url' : 'default', - autoCapitalize: 'none', - autoCorrect: false, - validate: validateField, - }, - ]} - onComplete={() => setFieldsDone(true)} + - {/* Help-Text */} + {/* 1. Type-Picker Pill */} + + + {/* 2. Input-Field */} + + + {inputLabel} + + { setInput(v); setError(null); }} + placeholder={inputPlaceholder} + placeholderTextColor={colors.textMuted} + keyboardType={kind === 'web' ? 'url' : 'email-address'} + autoCapitalize="none" + autoCorrect={false} + style={{ + backgroundColor: colors.surfaceElevated, + borderRadius: 10, + padding: 12, + fontSize: 14, + fontFamily: 'Nunito_400Regular', + color: colors.text, + borderWidth: 1, + borderColor: error ? '#dc2626' : colors.border, + }} + /> + {error && ( + + {error} + + )} + + + {/* 3. Help-Text */} - {/* Favicon-Preview (nur Web) */} - {kind === 'web' && ( + {/* 4. Preview-Card */} + {kind === 'web' ? ( - {normalizedWeb} + {normalizedWeb || inputPlaceholder} - )} - - {/* Mail-Typ Icon-Preview */} - {kind === 'mail' && ( + ) : ( @@ -252,7 +255,7 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P )} - {/* Warnung */} + {/* 5. Warning-Card */} - + - {/* Confirm-Checkbox */} + {/* 6. Confirm-Checkbox */} setConfirmPermanent((v) => !v)} activeOpacity={0.7} @@ -288,7 +290,6 @@ export function AddDomainSheet({ visible, tier, initialType, onClose, onAdd }: P alignItems: 'flex-start', gap: 10, paddingVertical: 4, - marginBottom: 14, }} > - {error && ( - + - {error} - - )} - - {/* Add-Button */} - - - {adding ? ( - - ) : ( - - {t('blocker.add_sheet_title')} + + + {t('common.cancel')} - )} - - - + + + + + + {adding ? ( + + ) : ( + + {t('blocker.add_sheet_cta')} + + )} + + + + ); } // ─── TypePicker ────────────────────────────────────────────────────────────── -function TypePicker({ kind, onChange }: { kind: InputKind; onChange: (k: InputKind) => void }) { +function TypePicker({ + kind, + onChange, + colors, +}: { + kind: InputKind; + onChange: (k: InputKind) => void; + colors: ColorScheme; +}) { const { t } = useTranslation(); - const colors = useColors(); return ( void; + kind: 'web' | 'mail'; onSubmit?: (id: string) => Promise<{ ok: boolean }>; - onRemove?: (id: string) => Promise<{ ok: boolean }>; onUpgradePro?: () => void; }; @@ -65,16 +49,16 @@ const STATUS_PRIORITY: Record = { approved: 99, }; -export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgradePro }: Props) { +export function DomainGrid({ domains, tier, kind, onSubmit, onUpgradePro }: Props) { const { t } = useTranslation(); const colors = useColors(); - // Filter by tab, then by status (exclude approved). Sort by status-priority, then newest-first. + const visible = useMemo(() => { return domains .filter((d) => { if (d.status === 'approved') return false; - if (activeTab === 'mail') { - return d.type === 'mail_domain' || d.type === 'mail_display_name'; + if (kind === 'mail') { + return d.type === 'mail_domain'; } return d.type === 'web' || !d.type; }) @@ -87,60 +71,10 @@ export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgrad const tb = b.addedAt ? new Date(b.addedAt).getTime() : 0; return tb - ta; }); - }, [domains, activeTab]); + }, [domains, kind]); return ( - {/* Header: Section-Title + Slot-Counter + Add-Button (inline, neben SlotPill) */} - - - {t('blocker.domain_section_title')} - - - - {onAdd && ( - - - - - - )} - - - - {/* Progress-Bar — 3-stufige Color-Schwelle: <60% grün, 60-90% orange, >=90% rot */} - {(() => { - const pct = (tier.usedSlots / tier.domainLimit) * 100; - const barColor = pct >= 90 ? '#dc2626' : pct >= 60 ? '#f59e0b' : '#16a34a'; - return ( - - - - ); - })()} - {/* Limit-Reached Upsell (nur Free) */} {tier.atLimit && tier.plan === 'free' && ( @@ -197,7 +131,7 @@ export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgrad textAlign: 'center', }} > - {activeTab === 'mail' ? t('blocker.empty_mail') : t('blocker.empty_web')} + {kind === 'mail' ? t('blocker.empty_mail') : t('blocker.empty_web')} ) : ( @@ -211,28 +145,6 @@ export function DomainGrid({ domains, tier, activeTab, onAdd, onSubmit, onUpgrad ); } -// ─── SlotPill ───────────────────────────────────────────────────────────── - -function SlotPill({ tier }: { tier: Tier }) { - const colors = useColors(); - const bg = tier.atLimit ? '#fee2e2' : colors.surfaceElevated; - const fg = tier.atLimit ? '#dc2626' : colors.textMuted; - return ( - - - {tier.usedSlots}/{tier.domainLimit} - - - ); -} - // ─── Tiles ──────────────────────────────────────────────────────────────── function DomainTilesGrid({ @@ -245,7 +157,7 @@ function DomainTilesGrid({ onSubmit?: (id: string) => Promise<{ ok: boolean }>; }) { // 3-Spalten-Grid via flex-wrap. Parent ScrollView (in blocker.tsx) handles scroll — - // KEIN nested ScrollView hier, sonst collabiert der Layout-Pass weil ScrollView + // KEIN nested ScrollView hier, sonst kollabiert der Layout-Pass weil ScrollView // inner-content-view keine definierte Width für %-basierte Tile-Widths hat. return ( @@ -281,21 +193,17 @@ function DomainTile({ const isLegend = tier.plan === 'legend'; - // statusColor wird auf Badge + Button angewendet. - // iOS-native: blue (active), orange (submitted), red (rejected). const statusColor = (() => { switch (domain.status) { case 'submitted': - return '#f59e0b'; // orange (Voting/Prüfung) + return '#f59e0b'; case 'rejected': - return '#FF3B30'; // iOS-red + return '#FF3B30'; default: - return '#007AFF'; // iOS-blue (active, "freigeben"-CTA) + return '#007AFF'; } })(); - // Time-Color: nur Status die Aufmerksamkeit brauchen (submitted/rejected) sind farbig. - // Active = neutral gray (settled state, kein Alarm-Indikator nötig). const timeColor = domain.status === 'active' ? '#a3a3a3' : statusColor; const badgeLabel = (() => { @@ -309,8 +217,6 @@ function DomainTile({ } })(); - // Tier-aware Confirm-Dialog vor Freigabe — Pro geht zu Community-Voting, - // Legend direkt zum ReBreak-Team. Animiertes Modal statt nativem Alert. const isResubmit = domain.status === 'rejected'; const confirmTitle = isLegend ? isResubmit @@ -363,15 +269,12 @@ function DomainTile({ borderColor: colors.border, borderRadius: 14, padding: 8, - // KEIN aspectRatio:1 mehr — der hat den Button auf 0 Höhe gepresst. - // minHeight statt fixer aspect-ratio: Tile darf wachsen wenn Button da ist, - // bleibt aber konsistent groß für visuelle Stabilität. minHeight: 130, opacity: isFreeAndUsed ? 0.55 : 1, gap: 6, }} > - {/* Top-Row: Zeit links · Badge rechts — beide in Status-Color (matcht Bottom-Button). */} + {/* Top-Row: Zeit links · Badge rechts */} - {/* Mitte: Icon + Domain-Name (zentriert, flex-1) */} + {/* Mitte: Icon + Domain-Name */} {domain.type === 'mail_domain' || domain.type === 'mail_display_name' ? ( - {/* Bottom-Slot: ALWAYS rendered Container (32px), Inhalt je nach Status. - * Garantiert konsistente Tile-Höhe + sichtbaren Button. */} + {/* Bottom-Slot: ALWAYS rendered Container (28px), Inhalt je nach Status. */} {showInPruefungBtn && ( - {/* Confirm-Modal vor Submit (statt nativer Alert.alert — selber animation-Style wie SuccessAlert) */} setConfirmVisible(false)} /> - {/* Success-Alert mit animiertem Check-Icon nach erfolgreichem Submit */} 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'), + (d) => d.status !== 'approved' && d.type === 'mail_domain', ).length, }; diff --git a/apps/rebreak-native/locales/de.json b/apps/rebreak-native/locales/de.json index 1786eea..ce2c6f0 100644 --- a/apps/rebreak-native/locales/de.json +++ b/apps/rebreak-native/locales/de.json @@ -322,17 +322,20 @@ "add_web_label": "Domain", "add_web_placeholder": "z.B. casino.com", "add_web_help": "Diese Webseite wird auf allen geschützten Geräten blockiert.", - "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_label": "E-Mail-Adresse oder Domain", + "add_mail_placeholder": "z.B. newsletter@casino.com oder casino.com", + "add_mail_help": "E-Mail-Adresse oder Mail-Domain. Wir blockieren alle Mails von diesem Absender.", "add_mail_invalid": "Bitte ein Muster eingeben.", + "add_sheet_cta": "Hinzufügen", "tabs_web": "Seiten", "tabs_mail": "Mails", + "section_domains": "Eigene Domains", + "section_mails": "Eigene 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." + "empty_mail": "Noch keine Mail-Domains. Tippe + um eine E-Mail-Adresse oder Domain zu blockieren." }, "mail": { "title": "Mail-Schutz", diff --git a/apps/rebreak-native/locales/en.json b/apps/rebreak-native/locales/en.json index 8b90e39..0bb54eb 100644 --- a/apps/rebreak-native/locales/en.json +++ b/apps/rebreak-native/locales/en.json @@ -322,17 +322,20 @@ "add_web_label": "Domain", "add_web_placeholder": "e.g. casino.com", "add_web_help": "This website will be blocked on all your protected devices.", - "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_label": "Email address or domain", + "add_mail_placeholder": "e.g. newsletter@casino.com or casino.com", + "add_mail_help": "Email address or mail domain. We block all emails from this sender.", "add_mail_invalid": "Please enter a pattern.", + "add_sheet_cta": "Add", "tabs_web": "Websites", "tabs_mail": "Emails", + "section_domains": "Your Domains", + "section_mails": "Your Email Filters", "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)." + "empty_mail": "No mail domains yet. Tap + to block an email address or domain." }, "mail": { "title": "Mail Shield", diff --git a/apps/rebreak-native/locales/fr.json b/apps/rebreak-native/locales/fr.json index a0cc9ab..02a03b4 100644 --- a/apps/rebreak-native/locales/fr.json +++ b/apps/rebreak-native/locales/fr.json @@ -322,17 +322,20 @@ "add_web_label": "Domaine", "add_web_placeholder": "ex. casino.com", "add_web_help": "Ce site sera bloqué sur tous vos appareils protégés.", - "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_label": "Adresse e-mail ou domaine", + "add_mail_placeholder": "ex. newsletter@casino.com ou casino.com", + "add_mail_help": "Adresse e-mail ou domaine mail. Nous bloquons tous les mails de cet expéditeur.", "add_mail_invalid": "Veuillez saisir un modèle.", + "add_sheet_cta": "Ajouter", "tabs_web": "Sites", "tabs_mail": "E-mails", + "section_domains": "Mes domaines", + "section_mails": "Mes filtres mail", "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)." + "empty_mail": "Aucun domaine mail. Appuyez sur + pour bloquer une adresse ou un domaine." }, "mail": { "title": "Protection Mail",