import { useCallback, useState } from 'react'; import { View, Text, ScrollView, FlatList, Pressable, RefreshControl, ActivityIndicator, } from 'react-native'; import { Ionicons } from '@expo/vector-icons'; import { useQuery, useQueryClient } from '@tanstack/react-query'; import { useTranslation } from 'react-i18next'; import { apiFetch } from '../../lib/api'; import { AppHeader } from '../../components/AppHeader'; import { ComposeCard } from '../../components/ComposeCard'; import { PostCard } from '../../components/PostCard'; import { PostCardSkeleton } from '../../components/PostCardSkeleton'; import { PostCommentsSheet } from '../../components/PostCommentsSheet'; import { useCommunityStore, type CommunityCategory, type CommunityPost } from '../../stores/community'; import { useCommunityRealtime } from '../../hooks/useCommunityRealtime'; import { colors } from '../../lib/theme'; type FilterChip = { value: CommunityCategory; label: string; icon: React.ComponentProps['name']; }; export default function HomeScreen() { const { t } = useTranslation(); const queryClient = useQueryClient(); // Granular selectors: subscribing to the whole store (incl. optimisticLikes) // would re-render the screen — and thus the FlatList — on every like. const activeCategory = useCommunityStore((s) => s.activeCategory); const setCategory = useCommunityStore((s) => s.setCategory); const FILTERS: FilterChip[] = [ { value: 'all', label: t('community.cat_all'), icon: 'grid-outline' }, { value: 'games', label: t('community.cat_games'), icon: 'trophy-outline' }, { value: 'domain_vote', label: t('community.cat_domain'), icon: 'shield-outline' }, { value: 'lyra', label: t('community.cat_lyra'), icon: 'sparkles-outline' }, { value: 'rebreak', label: t('community.cat_rebreak'), icon: 'megaphone-outline' }, ]; const [filterOpen, setFilterOpen] = useState(false); const [activeCommentsPostId, setActiveCommentsPostId] = useState(null); const { data: posts = [], isLoading, isRefetching, refetch } = useQuery({ queryKey: ['community-posts', activeCategory], queryFn: () => apiFetch(`/api/community/posts?category=${activeCategory}&limit=30`), staleTime: 60_000, }); // Realtime: live updates für Posts (likes/comments/neue Posts/domain-vote-Status) useCommunityRealtime(true); const toggleFilter = (value: CommunityCategory) => { const next = activeCategory === value ? 'all' : value; setCategory(next); setFilterOpen(false); }; // Stable callbacks — passed to memoized PostCards. Inline arrows would // bust React.memo on every parent render (which also happens on every // realtime patch since the posts array gets a new reference). const openComments = useCallback((postId: string) => { setActiveCommentsPostId(postId); }, []); const closeComments = useCallback(() => setActiveCommentsPostId(null), []); const keyExtractor = useCallback((item: CommunityPost) => item.id, []); const renderItem = useCallback( ({ item }: { item: CommunityPost }) => ( ), [openComments], ); return ( } ListHeaderComponent={ refetch()} /> {/* Filter toggle */} setFilterOpen((o) => !o)} className="flex-row items-center gap-1.5 self-start" style={({ pressed }) => ({ opacity: pressed ? 0.6 : 1 })} > {activeCategory !== 'all' && ( {FILTERS.find((f) => f.value === activeCategory)?.label} )} {filterOpen && ( {FILTERS.map((f) => { const active = activeCategory === f.value; return ( toggleFilter(f.value)} className={`flex-row items-center gap-1.5 h-8 px-3 rounded-full border ${ active ? 'bg-rebreak-500 border-rebreak-500' : 'bg-white border-neutral-200' }`} style={({ pressed }) => ({ opacity: pressed ? 0.7 : 1 })} > {f.label} ); })} )} {/* Skeleton */} {isLoading && ( )} } ListEmptyComponent={ isLoading ? null : ( {t('community.no_posts')} ) } renderItem={renderItem} /> ); }