feat(native): chat tab badge for unread DMs

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.
This commit is contained in:
chahinebrini 2026-05-16 01:53:03 +02:00
parent d11d548c10
commit ba170afd20
3 changed files with 51 additions and 0 deletions

View File

@ -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<DmConvUnreadSlice[]>({
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' }

View File

@ -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);

View File

@ -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")
}