import { useCallback, useMemo, useState } from "react"; import { View, Pressable, Text, Platform, Modal, Alert } from "react-native"; import { usePathname, useRouter } from "expo-router"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { AudioLines, Users, Plus, Download } from "lucide-react-native"; import { useRealtime } from "@/contexts/realtime-context"; import { useDaemonConnections } from "@/contexts/daemon-connections-context"; import { useFooterControls, FOOTER_HEIGHT } from "@/contexts/footer-controls-context"; import { RealtimeControls } from "./realtime-controls"; import { CreateAgentModal, ImportAgentModal } from "./create-agent-modal"; import Animated, { FadeIn, FadeOut, useAnimatedStyle, } from "react-native-reanimated"; import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; export function GlobalFooter() { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); const pathname = usePathname(); const router = useRouter(); const { isRealtimeMode, startRealtime } = useRealtime(); const { connectionStates } = useDaemonConnections(); const { controls } = useFooterControls(); const [showCreateModal, setShowCreateModal] = useState(false); const [showImportModal, setShowImportModal] = useState(false); const [showRealtimeHostPicker, setShowRealtimeHostPicker] = useState(false); // Guard Reanimated entry/exit transitions on Android to avoid ViewGroup.dispatchDraw crashes // tracked in react-native-reanimated#8422. const shouldDisableEntryExitAnimations = Platform.OS === "android"; const realtimeFadeIn = shouldDisableEntryExitAnimations ? undefined : FadeIn.duration(250); const realtimeFadeOut = shouldDisableEntryExitAnimations ? undefined : FadeOut.duration(250); // Determine current screen type const isAgentScreen = pathname?.startsWith("/agent/"); const hasRegisteredControls = !!controls; const showAgentControls = isAgentScreen && hasRegisteredControls; const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); const bottomInset = insets.bottom; const keyboardAnimatedStyle = useAnimatedStyle( () => { "worklet"; const absoluteHeight = Math.abs(keyboardHeight.value); const shift = Math.max(0, absoluteHeight - bottomInset); return { transform: [{ translateY: -shift }], }; }, [bottomInset], ); const realtimeEligibleHosts = useMemo(() => { return Array.from(connectionStates.values()).filter((entry) => entry.status === "online"); }, [connectionStates]); const handleStartRealtime = useCallback(() => { if (realtimeEligibleHosts.length === 0) { Alert.alert("No connected hosts", "Connect a host before starting realtime mode."); return; } if (realtimeEligibleHosts.length === 1) { void startRealtime(realtimeEligibleHosts[0].daemon.id).catch((error) => { console.error("[GlobalFooter] Failed to start realtime", error); Alert.alert("Realtime failed", "Unable to start realtime mode for this host."); }); return; } setShowRealtimeHostPicker(true); }, [realtimeEligibleHosts, startRealtime]); const handleSelectRealtimeHost = useCallback( (daemonId: string) => { setShowRealtimeHostPicker(false); void startRealtime(daemonId).catch((error) => { console.error("[GlobalFooter] Failed to start realtime", error); Alert.alert("Realtime failed", "Unable to start realtime mode for this host."); }); }, [startRealtime] ); const handleDismissHostPicker = useCallback(() => { setShowRealtimeHostPicker(false); }, []); if (showAgentControls) { return ( {controls} ); } if (isAgentScreen) { return null; } // For home and orchestrator screens, show action buttons with realtime stacked on top const nonAgentFooterHeight = isRealtimeMode ? FOOTER_HEIGHT * 2 + insets.bottom : FOOTER_HEIGHT + insets.bottom; const iconSize = 24; const iconStyle = { width: iconSize, height: iconSize }; return ( <> {/* Realtime controls - only visible when active */} {isRealtimeMode && ( )} {/* Action menu */} router.push("/")} style={({ pressed }) => [ styles.footerButton, pressed && styles.buttonPressed, ]} > Agents setShowImportModal(true)} style={({ pressed }) => [ styles.footerButton, pressed && styles.buttonPressed, ]} > Import { console.log("[GlobalFooter] New Agent button pressed"); setShowCreateModal(true); }} style={({ pressed }) => [ styles.footerButton, pressed && styles.buttonPressed, ]} > New Agent [ styles.footerButton, realtimeEligibleHosts.length === 0 && styles.buttonDisabled, pressed && realtimeEligibleHosts.length === 0 && styles.buttonPressed, ]} > Realtime setShowCreateModal(false)} /> setShowImportModal(false)} /> Choose a host {realtimeEligibleHosts.map((entry) => ( handleSelectRealtimeHost(entry.daemon.id)} > {entry.daemon.label} ))} Cancel ); } const styles = StyleSheet.create((theme) => ({ container: { backgroundColor: theme.colors.background, borderTopWidth: theme.borderWidth[1], borderTopColor: theme.colors.border, }, nonAgentContent: { flexDirection: "column", }, realtimeSection: { height: FOOTER_HEIGHT, borderBottomWidth: theme.borderWidth[1], borderBottomColor: theme.colors.border, }, actionButtonContainer: { flexDirection: "row", padding: theme.spacing[4], gap: theme.spacing[3], height: FOOTER_HEIGHT, }, footerButton: { flex: 1, flexDirection: "column", alignItems: "center", justifyContent: "center", paddingVertical: theme.spacing[3], gap: theme.spacing[1], }, footerIconWrapper: { width: 28, height: 28, alignItems: "center", justifyContent: "center", }, footerButtonText: { color: theme.colors.foreground, fontSize: theme.fontSize.xs, fontWeight: theme.fontWeight.normal, }, buttonDisabled: { opacity: 0.5, }, buttonPressed: { opacity: 0.5, }, hostPickerOverlay: { flex: 1, backgroundColor: "rgba(0,0,0,0.5)", justifyContent: "flex-end", }, hostPickerBackdrop: { flex: 1, }, hostPickerContainer: { backgroundColor: theme.colors.card, padding: theme.spacing[4], borderTopLeftRadius: theme.borderRadius.xl, borderTopRightRadius: theme.borderRadius.xl, gap: theme.spacing[3], }, hostPickerTitle: { fontSize: theme.fontSize.lg, fontWeight: theme.fontWeight.semibold, color: theme.colors.foreground, textAlign: "center", }, hostPickerButton: { paddingVertical: theme.spacing[3], borderRadius: theme.borderRadius.lg, backgroundColor: theme.colors.muted, }, hostPickerButtonText: { color: theme.colors.foreground, fontSize: theme.fontSize.sm, textAlign: "center", }, hostPickerCancel: { paddingVertical: theme.spacing[3], }, hostPickerCancelText: { color: theme.colors.mutedForeground, fontSize: theme.fontSize.sm, textAlign: "center", }, }));