From ba170afd20be7ad4436560dc0a04dfbac6860a9d Mon Sep 17 00:00:00 2001 From: chahinebrini Date: Sat, 16 May 2026 01:53:03 +0200 Subject: [PATCH] feat(native): chat tab badge for unread DMs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a tabBarBadge on the bottom Chat tab driven by the same dm-conversations query the chat screen already uses — React Query dedupes the call. Badge shows the unread total (capped to "99+") and disappears when 0. Query is gated on session so unauthenticated launches don't fire it. --- apps/rebreak-native/app/(app)/_layout.tsx | 15 +++++++++ .../20260516_custom_domain_type/migration.sql | 31 +++++++++++++++++++ backend/prisma/schema.prisma | 5 +++ 3 files changed, 51 insertions(+) create mode 100644 backend/prisma/migrations/20260516_custom_domain_type/migration.sql diff --git a/apps/rebreak-native/app/(app)/_layout.tsx b/apps/rebreak-native/app/(app)/_layout.tsx index f453f9f..fea154d 100644 --- a/apps/rebreak-native/app/(app)/_layout.tsx +++ b/apps/rebreak-native/app/(app)/_layout.tsx @@ -12,6 +12,9 @@ import { MailConsentReminderSheet } from '../../components/mail/MailConsentRemin import { protection } from '../../lib/protection'; import { preloadTabIcons, getTabIcon } from '../../lib/tabIcons'; import { apiFetch } from '../../lib/api'; +import { useQuery } from '@tanstack/react-query'; + +type DmConvUnreadSlice = { unreadCount?: number }; export default function AppLayout() { const router = useRouter(); @@ -26,6 +29,17 @@ export default function AppLayout() { const rearmInFlightRef = useRef(false); const bypassNotifiedRef = useRef(false); + // Unread DMs → badge on the Chat tab. Same query key chat.tsx uses, so + // React Query dedupes (no double fetch when both layouts mount). + const { data: dmConvs = [] } = useQuery({ + queryKey: ['dm-conversations'], + queryFn: () => apiFetch('/api/chat/dm-conversations'), + staleTime: 30_000, + enabled: !!session, + }); + const unreadDms = dmConvs.reduce((sum, c) => sum + (c.unreadCount ?? 0), 0); + const chatBadge = unreadDms > 0 ? (unreadDms > 99 ? '99+' : String(unreadDms)) : undefined; + // Android-Tab-Icons müssen async aus Ionicons-Font generiert werden (kein // SF-Symbol-Support). preloadTabIcons() läuft schon beim Modul-Import — hier // nur den ready-State tracken damit wir re-rendern wenn der Cache fertig ist. @@ -207,6 +221,7 @@ export default function AppLayout() { name="chat" options={{ title: t('tabs.chat'), + tabBarBadge: chatBadge, tabBarIcon: () => Platform.OS === 'ios' ? { sfSymbol: 'bubble.left.and.bubble.right.fill' } diff --git a/backend/prisma/migrations/20260516_custom_domain_type/migration.sql b/backend/prisma/migrations/20260516_custom_domain_type/migration.sql new file mode 100644 index 0000000..d505c27 --- /dev/null +++ b/backend/prisma/migrations/20260516_custom_domain_type/migration.sql @@ -0,0 +1,31 @@ +-- Migration: 20260516_custom_domain_type +-- +-- Erweitert user_custom_domains um das `type`-Feld. +-- Drei erlaubte Typen: +-- 'web' — Web-Domain-Block (bisheriges Verhalten, Default) +-- 'mail_domain' — Sender-Domain-Block im Mail-Filter (analog web, nutzt Domain-Match-Layer) +-- 'mail_display_name' — Sender-Display-Name-Pattern-Block (Substring-Match, case-insensitive) +-- +-- Slot-Pool: alle drei Types teilen den gleichen Slot-Pool pro Plan +-- (Free: 5, Pro: 5, Legend: 10 — countActiveCustomDomains() zählt alle Types zusammen). +-- +-- DSGVO-Hinweis: +-- Display-Name-Patterns (type='mail_display_name') sind reine Heuristik-Patterns +-- die vom User selbst eingetragen werden (z.B. "EXTRASPIN"). +-- Sie gelten NICHT als personenbezogene Daten (Art. 4 DSGVO) — +-- es handelt sich um selbst-definierte Schlagworte, nicht um identifizierbare PII. +-- Die Patterns unterliegen dennoch Art. 17 DSGVO (Löschrecht via CASCADE userId). +-- +-- Non-breaking: alle existierenden Rows erhalten type='web' als Default. + +ALTER TABLE rebreak.user_custom_domains + ADD COLUMN IF NOT EXISTS type TEXT NOT NULL DEFAULT 'web'; + +ALTER TABLE rebreak.user_custom_domains + ADD CONSTRAINT user_custom_domains_type_check + CHECK (type IN ('web', 'mail_domain', 'mail_display_name')); + +-- Index für effiziente type-basierte Abfragen pro User +-- (getCustomMailDisplayNames läuft bei jedem Mail-Scan) +CREATE INDEX IF NOT EXISTS user_custom_domains_user_type_idx + ON rebreak.user_custom_domains (user_id, type); diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index 9564580..b8bb6d2 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -371,12 +371,17 @@ model UserCustomDomain { source String @default("manual") // "active" | "submitted" | "approved" | "rejected" status String @default("active") + // "web" | "mail_domain" | "mail_display_name" + // Alle Types teilen den gleichen Slot-Pool (countActiveCustomDomains() zählt total). + // mail_display_name: Substring-Heuristik gegen Sender-Display-Name (kein PII, Art. 4 DSGVO) + type String @default("web") postId String? @map("post_id") @db.Uuid addedAt DateTime @default(now()) @map("added_at") submission DomainSubmission? @@unique([userId, domain]) + @@index([userId, type]) @@map("user_custom_domains") @@schema("rebreak") }