Wave 2 = ALLE app-files die in Wave 1 noch hardcoded waren. Komplette App-weit
theme-aware-Migration jetzt durch. Legacy `import { colors }` flat export
vollständig eliminiert.
Migrated this wave:
Top-level Screens:
- app/urge.tsx (makeStyles factory mit ~20 colors)
- app/room.tsx + dm.tsx + games.tsx
- app/(app)/chat.tsx + mail.tsx + coach.tsx + notifications.tsx
- app/profile/[userId].tsx + profile/edit.tsx (INPUT_STYLE in body moved)
- app/debug.tsx + auth/callback.tsx
Blocker (7):
- AddDomainSheet, CooldownBanner, DeactivationExplainerSheet, DomainGrid,
ProtectionCard, ProtectionDetailsSheet, ProtectionLockedCard
Mail (3):
- ConnectMailSheet, EditMailAccountSheet, MailEmptyState
Chat (1):
- ChatBubble, ChatInput
Community/Posts/Notifications:
- PostCard, PostCardSkeleton, ComposeCard, PostCommentsSheet
- NotificationsDropdown
- StreakBadge (Nativewind classes durch inline dynamic styles ersetzt)
Reusable Sheets:
- WheelPickerModal, OptionsBottomSheet, DeviceLimitReachedSheet
Urge subsystem (5):
- InlineRatingDrawer, ShareSuccessDrawer, UrgeStats, SosFeedbackModal,
Breathing
Profile components:
- DigaMissionBanner
Pattern: useColors() hook in component body, makeStyles(colors) factory wo
StyleSheet.create vorher hardcoded war. 11 base-tokens (bg/surface/
surfaceElevated/border/text/textMuted/brandOrange/brandBlue/success/error/
warning) nutzen colors.light vs colors.dark scheme.
Bewusst NICHT migriert (semantic colors):
- DigaMissionBanner amber (#fffbeb, #854d0e) — DiGA-brand, nicht neutral
- Lyra-thinking #3b82f6 in urge.tsx — Lyra-brand-color
- scrollDownBtn #374151 — intentional dark floating-button
TS clean. Test: Settings → Theme → Dark — alle screens sollen jetzt dunkel
werden ohne white-flashes.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
155 lines
5.0 KiB
TypeScript
155 lines
5.0 KiB
TypeScript
import { useEffect } from 'react';
|
|
import { View, Text, FlatList, Pressable, RefreshControl } from 'react-native';
|
|
import { SafeAreaView } from 'react-native-safe-area-context';
|
|
import { useRouter } from 'expo-router';
|
|
import { Ionicons } from '@expo/vector-icons';
|
|
import { HeroShieldCheck } from '../../components/HeroShieldCheck';
|
|
import { useTranslation } from 'react-i18next';
|
|
import { EmptyState } from '../../components/EmptyState';
|
|
import { useNotificationStore, type AppNotification } from '../../stores/notifications';
|
|
import { useColors } from '../../lib/theme';
|
|
|
|
export default function NotificationsScreen() {
|
|
const router = useRouter();
|
|
const { t } = useTranslation();
|
|
const colors = useColors();
|
|
const items = useNotificationStore((s) => s.items);
|
|
const loaded = useNotificationStore((s) => s.loaded);
|
|
const load = useNotificationStore((s) => s.load);
|
|
const markRead = useNotificationStore((s) => s.markRead);
|
|
const remove = useNotificationStore((s) => s.remove);
|
|
|
|
useEffect(() => {
|
|
load();
|
|
const tm = setTimeout(() => {
|
|
markRead();
|
|
}, 400);
|
|
return () => clearTimeout(tm);
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, []);
|
|
|
|
return (
|
|
<SafeAreaView style={{ flex: 1, backgroundColor: colors.bg }} edges={['top']}>
|
|
<View style={{ flexDirection: 'row', alignItems: 'center', gap: 12, paddingHorizontal: 20, paddingTop: 12, paddingBottom: 12, borderBottomWidth: 1, borderBottomColor: colors.border }}>
|
|
<Pressable
|
|
onPress={() => router.back()}
|
|
style={{ width: 36, height: 36, borderRadius: 18, backgroundColor: colors.surfaceElevated, borderWidth: 1, borderColor: colors.border, alignItems: 'center', justifyContent: 'center' }}
|
|
>
|
|
<Ionicons name="arrow-back" size={18} color={colors.textMuted} />
|
|
</Pressable>
|
|
<Text
|
|
style={{ color: colors.text, fontSize: 18, flex: 1, fontFamily: 'Nunito_700Bold' }}
|
|
>
|
|
{t('notifications.title')}
|
|
</Text>
|
|
</View>
|
|
|
|
{items.length === 0 ? (
|
|
<EmptyState
|
|
icon="notifications-off-outline"
|
|
title={t('notifications.empty_title')}
|
|
subtitle={t('notifications.empty_subtitle')}
|
|
/>
|
|
) : (
|
|
<FlatList
|
|
data={items}
|
|
keyExtractor={(n) => n.id}
|
|
contentContainerStyle={{ paddingVertical: 8 }}
|
|
refreshControl={
|
|
<RefreshControl
|
|
refreshing={!loaded}
|
|
onRefresh={load}
|
|
tintColor="#007AFF"
|
|
/>
|
|
}
|
|
renderItem={({ item }) => (
|
|
<NotificationRow
|
|
notif={item}
|
|
onPress={() => {
|
|
if (item.postId) {
|
|
router.push(`/?postId=${item.postId}` as never);
|
|
}
|
|
}}
|
|
onDelete={() => remove(item.id)}
|
|
/>
|
|
)}
|
|
/>
|
|
)}
|
|
</SafeAreaView>
|
|
);
|
|
}
|
|
|
|
function NotificationRow({
|
|
notif,
|
|
onPress,
|
|
onDelete,
|
|
}: {
|
|
notif: AppNotification;
|
|
onPress: () => void;
|
|
onDelete: () => void;
|
|
}) {
|
|
const colors = useColors();
|
|
const isUnread = !notif.readAt;
|
|
return (
|
|
<Pressable
|
|
onPress={onPress}
|
|
style={({ pressed }) => ({
|
|
opacity: pressed ? 0.7 : 1,
|
|
})}
|
|
>
|
|
<View
|
|
style={{
|
|
flexDirection: 'row',
|
|
alignItems: 'flex-start',
|
|
paddingHorizontal: 16,
|
|
paddingVertical: 12,
|
|
borderBottomWidth: 1,
|
|
borderBottomColor: colors.border,
|
|
backgroundColor: isUnread ? colors.surface : colors.bg,
|
|
}}
|
|
>
|
|
{/* Pure-Icon — KEIN bg-Circle (User-Wunsch: kein extra Rand). */}
|
|
<View style={{ width: 36, alignItems: 'center', justifyContent: 'center', marginRight: 12 }}>
|
|
{notif.type === 'domain_accepted' ? (
|
|
<HeroShieldCheck size={22} color="#16a34a" />
|
|
) : (
|
|
<Ionicons name={iconForType(notif.type)} size={22} color="#d97706" />
|
|
)}
|
|
</View>
|
|
<View style={{ flex: 1, minWidth: 0, marginRight: 8 }}>
|
|
<Text
|
|
style={{ fontSize: 13, fontFamily: 'Nunito_700Bold', color: colors.text }}
|
|
numberOfLines={1}
|
|
>
|
|
{notif.actorName}
|
|
</Text>
|
|
{notif.preview && (
|
|
<Text
|
|
style={{
|
|
fontSize: 12,
|
|
fontFamily: 'Nunito_400Regular',
|
|
color: colors.textMuted,
|
|
marginTop: 2,
|
|
}}
|
|
numberOfLines={2}
|
|
>
|
|
{notif.preview}
|
|
</Text>
|
|
)}
|
|
</View>
|
|
<Pressable onPress={onDelete} hitSlop={8}>
|
|
<Ionicons name="close" size={16} color="#a3a3a3" />
|
|
</Pressable>
|
|
</View>
|
|
</Pressable>
|
|
);
|
|
}
|
|
|
|
function iconForType(type: string): React.ComponentProps<typeof Ionicons>['name'] {
|
|
if (type.includes('like')) return 'heart';
|
|
if (type.includes('comment')) return 'chatbubble';
|
|
if (type.includes('follow')) return 'person-add';
|
|
if (type.includes('domain')) return 'shield-checkmark';
|
|
return 'notifications';
|
|
}
|