import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query"; import React, { useEffect, useMemo, useRef, useState } from "react"; import { FlatList, StyleSheet, useWindowDimensions, View } from "react-native"; import { notificationApi, notificationKeys } from "../notification-api"; import { useNotificationAction } from "../notification-actions"; import { typeMap } from "../notification-presenter"; import { layout, spacing } from "../theme"; import type { NotificationItem } from "../types"; import { ErrorNotice, Loading } from "../ui"; import { NotificationCard } from "./NotificationCard"; export function NotificationCarousel({ authenticated, autoplay = false, autoplayIntervalMs = 5000, requireAuth, }: { authenticated: boolean; autoplay?: boolean; autoplayIntervalMs?: number; requireAuth: (afterAuth?: () => Promise) => void; }) { const client = useQueryClient(); const list = useRef>(null); const [activeIndex, setActiveIndex] = useState(0); const [actionError, setActionError] = useState(); const [hiddenGuestIds, setHiddenGuestIds] = useState>(() => new Set()); const window = useWindowDimensions(); const cardWidth = Math.max(0, Math.min(window.width, layout.maxWidth) - spacing.lg * 2); const catalog = useQuery({ queryKey: notificationKeys.catalog, queryFn: notificationApi.catalog, staleTime: Infinity, }); const notifications = useQuery({ queryKey: notificationKeys.home(authenticated), queryFn: authenticated ? () => notificationApi.list("home") : notificationApi.guestHome, }); const byCode = useMemo(() => typeMap(catalog.data ?? []), [catalog.data]); const action = useNotificationAction({ authenticated, requireAuth, onError: setActionError }); const hide = useMutation({ mutationFn: notificationApi.hide, onSuccess: async () => { await Promise.all([ client.invalidateQueries({ queryKey: notificationKeys.home(true) }), client.invalidateQueries({ queryKey: notificationKeys.center }), ]); }, onError: setActionError, }); const data = (notifications.data ?? []).filter((item) => authenticated || !hiddenGuestIds.has(item.id)); const goTo = (index: number) => { if (data.length < 2) return; const next = (index + data.length) % data.length; setActiveIndex(next); list.current?.scrollToIndex({ index: next, animated: true }); }; useEffect(() => { if (!autoplay || data.length < 2) return; const timer = setInterval(() => { setActiveIndex((current) => { const next = (current + 1) % data.length; list.current?.scrollToIndex({ index: next, animated: true }); return next; }); }, Math.max(1000, autoplayIntervalMs)); return () => clearInterval(timer); }, [autoplay, autoplayIntervalMs, data.length]); useEffect(() => { if (activeIndex < data.length) return; const next = Math.max(data.length - 1, 0); setActiveIndex(next); list.current?.scrollToIndex({ index: next, animated: false }); }, [activeIndex, data.length]); if (notifications.isLoading || catalog.isLoading) { return ; } if (notifications.error || catalog.error) { return ( { void notifications.refetch(); void catalog.refetch(); }} /> ); } if (!data.length) return null; return ( ({ length: cardWidth, offset: cardWidth * index, index })} keyExtractor={(item) => item.id} style={local.carousel} onMomentumScrollEnd={(event) => { if (cardWidth > 0) { setActiveIndex(Math.min(data.length - 1, Math.round(event.nativeEvent.contentOffset.x / cardWidth))); } }} renderItem={({ item }) => ( void action(item, byCode.get(item.notification_type))} onNext={() => goTo(activeIndex + 1)} onPrevious={() => goTo(activeIndex - 1)} position={activeIndex + 1} total={data.length} width={cardWidth} onHide={() => { if (authenticated) { hide.mutate(item.id); } else { setHiddenGuestIds((current) => new Set(current).add(item.id)); } }} /> )} showsHorizontalScrollIndicator={false} /> {Boolean(actionError) && } ); } const local = StyleSheet.create({ section: { paddingVertical: spacing.md }, carousel: { marginHorizontal: spacing.lg }, state: { paddingHorizontal: spacing.lg, paddingVertical: spacing.md }, error: { paddingHorizontal: spacing.lg, paddingTop: spacing.sm }, });