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:
parent
d11d548c10
commit
ba170afd20
@ -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' }
|
||||
|
||||
@ -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);
|
||||
@ -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")
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user