refactor: simplify footer architecture by removing global footer pattern
Replace portal/registry pattern with direct rendering where each screen owns its footer. This eliminates FooterControlsContext indirection and makes the code follow standard React composition patterns. - Remove GlobalFooter and FooterControlsProvider from _layout.tsx - Add HomeFooter component rendered directly in home screen - Render AgentInputArea directly in agent screen (not via registration) - Add keyboard animation and safe area handling to AgentInputArea - Move FOOTER_HEIGHT constant to shared constants/layout.ts - Delete footer-controls-context.tsx and global-footer.tsx 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -20,9 +20,11 @@ import Animated, {
|
||||
FadeIn,
|
||||
FadeOut,
|
||||
} from "react-native-reanimated";
|
||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||
import { useRealtime } from "@/contexts/realtime-context";
|
||||
import { useDictation } from "@/hooks/use-dictation";
|
||||
import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
|
||||
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||
import { VoiceNoteRecordingOverlay } from "./voice-note-recording-overlay";
|
||||
import { DictationStatusNotice, type DictationToastVariant } from "./dictation-status-notice";
|
||||
import { generateMessageId } from "@/types/stream";
|
||||
@@ -85,6 +87,8 @@ type DictationToastConfig = {
|
||||
|
||||
export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
||||
const { theme } = useUnistyles();
|
||||
const insets = useSafeAreaInsets();
|
||||
const { height: keyboardHeight } = useReanimatedKeyboardAnimation();
|
||||
|
||||
const ws = useSessionStore((state) => state.sessions[serverId]?.ws);
|
||||
|
||||
@@ -825,6 +829,15 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
||||
};
|
||||
});
|
||||
|
||||
const keyboardAnimatedStyle = useAnimatedStyle(() => {
|
||||
"worklet";
|
||||
const absoluteHeight = Math.abs(keyboardHeight.value);
|
||||
const shift = Math.max(0, absoluteHeight - insets.bottom);
|
||||
return {
|
||||
transform: [{ translateY: -shift }],
|
||||
};
|
||||
});
|
||||
|
||||
async function handleRealtimePress() {
|
||||
try {
|
||||
if (isRealtimeMode) {
|
||||
@@ -923,7 +936,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<Animated.View style={[styles.container, { paddingBottom: insets.bottom }, keyboardAnimatedStyle]}>
|
||||
{/* Border separator */}
|
||||
<View style={styles.borderSeparator} />
|
||||
|
||||
@@ -1104,7 +1117,7 @@ export function AgentInputArea({ agentId, serverId }: AgentInputAreaProps) {
|
||||
</View>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
+6
-34
@@ -1,12 +1,12 @@
|
||||
import { useCallback, useMemo, useState } from "react";
|
||||
import { View, Pressable, Text, Platform, Modal, Alert } from "react-native";
|
||||
import { usePathname, useRouter } from "expo-router";
|
||||
import { 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 { FOOTER_HEIGHT } from "@/constants/layout";
|
||||
import { RealtimeControls } from "./realtime-controls";
|
||||
import { CreateAgentModal, ImportAgentModal } from "./create-agent-modal";
|
||||
import Animated, {
|
||||
@@ -16,14 +16,12 @@ import Animated, {
|
||||
} from "react-native-reanimated";
|
||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||
|
||||
export function GlobalFooter() {
|
||||
export function HomeFooter() {
|
||||
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);
|
||||
@@ -33,12 +31,6 @@ export function GlobalFooter() {
|
||||
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;
|
||||
|
||||
@@ -76,7 +68,7 @@ export function GlobalFooter() {
|
||||
}
|
||||
if (realtimeEligibleHosts.length === 1) {
|
||||
void startRealtime(realtimeEligibleHosts[0].daemon.id).catch((error) => {
|
||||
console.error("[GlobalFooter] Failed to start realtime", error);
|
||||
console.error("[HomeFooter] Failed to start realtime", error);
|
||||
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
|
||||
});
|
||||
return;
|
||||
@@ -88,7 +80,7 @@ export function GlobalFooter() {
|
||||
(daemonId: string) => {
|
||||
setShowRealtimeHostPicker(false);
|
||||
void startRealtime(daemonId).catch((error) => {
|
||||
console.error("[GlobalFooter] Failed to start realtime", error);
|
||||
console.error("[HomeFooter] Failed to start realtime", error);
|
||||
Alert.alert("Realtime failed", "Unable to start realtime mode for this host.");
|
||||
});
|
||||
},
|
||||
@@ -99,26 +91,6 @@ export function GlobalFooter() {
|
||||
setShowRealtimeHostPicker(false);
|
||||
}, []);
|
||||
|
||||
if (showAgentControls) {
|
||||
return (
|
||||
<Animated.View
|
||||
style={[
|
||||
styles.container,
|
||||
{
|
||||
paddingBottom: insets.bottom,
|
||||
},
|
||||
keyboardAnimatedStyle,
|
||||
]}
|
||||
>
|
||||
{controls}
|
||||
</Animated.View>
|
||||
);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -191,7 +163,7 @@ export function GlobalFooter() {
|
||||
|
||||
<Pressable
|
||||
onPress={() => {
|
||||
console.log("[GlobalFooter] New Agent button pressed");
|
||||
console.log("[HomeFooter] New Agent button pressed");
|
||||
setShowCreateModal(true);
|
||||
}}
|
||||
style={({ pressed }) => [
|
||||
@@ -3,7 +3,7 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { MicOff, Square } from "lucide-react-native";
|
||||
import { VolumeMeter } from "./volume-meter";
|
||||
import { useRealtime } from "@/contexts/realtime-context";
|
||||
import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
|
||||
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||
|
||||
const CONTROL_BUTTON_SIZE = 48;
|
||||
const VERTICAL_PADDING = (FOOTER_HEIGHT - CONTROL_BUTTON_SIZE) / 2;
|
||||
|
||||
@@ -2,7 +2,7 @@ import { View, Text, Pressable, ActivityIndicator } from "react-native";
|
||||
import { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||
import { X, ArrowUp, RefreshCcw } from "lucide-react-native";
|
||||
import { VolumeMeter } from "./volume-meter";
|
||||
import { FOOTER_HEIGHT } from "@/contexts/footer-controls-context";
|
||||
import { FOOTER_HEIGHT } from "@/constants/layout";
|
||||
import type { DictationStatus } from "@/hooks/use-dictation";
|
||||
|
||||
interface VoiceNoteRecordingOverlayProps {
|
||||
|
||||
Reference in New Issue
Block a user