import { useState, useRef, useMemo, useCallback, Dispatch, SetStateAction } from "react"; import { View, Text, StyleSheet, PanResponder, Animated, Dimensions, FlatList, ViewStyle, } from "react-native"; import { Mutation } from "@tanstack/react-query"; import MutationButton from "./MutationButton"; import MutationInformation from "./MutationInformation"; import useAllMutations from "../../hooks/useAllMutations"; import { gameUIColors } from "@react-buoy/shared-ui"; interface Props { selectedMutation: Mutation | undefined; setSelectedMutation: Dispatch< SetStateAction >; activeFilter?: string | null; hideInfoPanel?: boolean; contentContainerStyle?: ViewStyle; searchText?: string; } /** * Virtualized list of mutations with filtering and selection support for the mutation browser. */ export default function MutationsList({ selectedMutation, setSelectedMutation, activeFilter, hideInfoPanel = false, contentContainerStyle, searchText = "", }: Props) { const { mutations: allmutations } = useAllMutations(); // Helper function to get mutation status for filtering const getMutationStatus = (mutation: Mutation) => { if (mutation.state.isPaused) return "paused"; const status = mutation.state.status; return status; // 'idle', 'pending', 'success', 'error' }; // Filter mutations based on active filter and search text const filteredMutations = useMemo(() => { let filtered = allmutations; // Apply status filter if (activeFilter) { filtered = filtered.filter((mutation) => { const status = getMutationStatus(mutation); return status === activeFilter; }); } // Apply search filter on mutation keys if (searchText) { const searchLower = searchText.toLowerCase(); filtered = filtered.filter((mutation) => { const mutationKey = mutation.options.mutationKey; if (!mutationKey) return false; const keys = Array.isArray(mutationKey) ? mutationKey : [mutationKey]; const keyString = keys .filter((k) => k != null) .map((k) => String(k)) .join(" ") .toLowerCase(); return keyString.includes(searchLower); }); } return filtered; }, [allmutations, activeFilter, searchText]); // Height management for resizable mutation information panel const screenHeight = Dimensions.get("window").height; const defaultInfoHeight = screenHeight * 0.4; // 40% of screen height const minInfoHeight = 150; const maxInfoHeight = screenHeight * 0.7; // 70% of screen height const infoHeightAnim = useRef(new Animated.Value(defaultInfoHeight)).current; const [, setCurrentInfoHeight] = useState(defaultInfoHeight); const currentInfoHeightRef = useRef(defaultInfoHeight); // Pan responder for dragging the mutation information panel const infoPanResponder = useRef( PanResponder.create({ onMoveShouldSetPanResponder: (evt, gestureState) => { return ( Math.abs(gestureState.dy) > Math.abs(gestureState.dx) && Math.abs(gestureState.dy) > 10 ); }, onPanResponderGrant: () => { infoHeightAnim.stopAnimation((value) => { setCurrentInfoHeight(value); currentInfoHeightRef.current = value; infoHeightAnim.setValue(value); }); }, onPanResponderMove: (evt, gestureState) => { // Use the ref value which is always current const newHeight = currentInfoHeightRef.current - gestureState.dy; const clampedHeight = Math.max( minInfoHeight, Math.min(maxInfoHeight, newHeight), ); infoHeightAnim.setValue(clampedHeight); }, onPanResponderRelease: (evt, gestureState) => { const finalHeight = Math.max( minInfoHeight, Math.min( maxInfoHeight, currentInfoHeightRef.current - gestureState.dy, ), ); setCurrentInfoHeight(finalHeight); currentInfoHeightRef.current = finalHeight; Animated.timing(infoHeightAnim, { toValue: finalHeight, duration: 200, useNativeDriver: false, }).start(() => { // Ensure the animated value and state are perfectly synced after animation infoHeightAnim.setValue(finalHeight); setCurrentInfoHeight(finalHeight); currentInfoHeightRef.current = finalHeight; }); }, }), ).current; // Optimize FlatList performance - memoize renderItem to prevent re-renders const renderMutation = useCallback(({ item }: { item: Mutation }) => ( ), [selectedMutation, setSelectedMutation]); return ( {filteredMutations.length > 0 ? ( `${item.mutationId}-${item.state.submittedAt}-${item.state.status}-${index}`} showsVerticalScrollIndicator removeClippedSubviews contentContainerStyle={contentContainerStyle || styles.listContent} initialNumToRender={10} maxToRenderPerBatch={10} windowSize={10} scrollEnabled={false} /> ) : ( {activeFilter ? `No ${activeFilter} mutations found` : "No mutations found"} )} {selectedMutation && !hideInfoPanel && ( {/* Drag handle for resizing */} )} ); } const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: gameUIColors.background, }, listWrapper: { flex: 1, }, listContent: { backgroundColor: gameUIColors.background, paddingHorizontal: 8, paddingTop: 8, }, mutationInfo: { borderTopWidth: 1, borderTopColor: gameUIColors.border + "40", backgroundColor: gameUIColors.background, }, dragHandle: { height: 20, justifyContent: "center", alignItems: "center", backgroundColor: gameUIColors.panel, borderBottomWidth: 1, borderBottomColor: gameUIColors.border + "40", }, dragIndicator: { width: 40, height: 4, backgroundColor: gameUIColors.border, borderRadius: 2, }, mutationInfoContent: { flex: 1, backgroundColor: gameUIColors.background, }, emptyContainer: { flex: 1, justifyContent: "center", alignItems: "center", padding: 32, backgroundColor: gameUIColors.panel, margin: 16, borderRadius: 8, borderWidth: 1, borderColor: gameUIColors.border + "40", }, emptyText: { color: gameUIColors.muted, fontSize: 14, textAlign: "center", fontFamily: "monospace", letterSpacing: 0.5, }, });