import React, { useCallback, useRef, useState } from "react"; import { Animated, Pressable, Text, View } from "react-native"; import * as Haptics from "expo-haptics"; import { useAudioRecorder, RecordingPresets, requestRecordingPermissionsAsync, setAudioModeAsync, } from "expo-audio"; import { stopPlayback } from "../../services/audio"; interface VoiceButtonProps { onVoiceRecorded: (uri: string) => void; } const VOICE_BUTTON_SIZE = 72; /** * Tap-to-toggle voice button using expo-audio recording. * Records audio and returns the file URI for the caller to send. * - Tap once: start recording * - Tap again: stop and send * - Long-press while recording: cancel (discard) */ export function VoiceButton({ onVoiceRecorded }: VoiceButtonProps) { const [isRecording, setIsRecording] = useState(false); const pulseAnim = useRef(new Animated.Value(1)).current; const glowAnim = useRef(new Animated.Value(0)).current; const pulseLoop = useRef(null); const recorder = useAudioRecorder(RecordingPresets.HIGH_QUALITY); const startPulse = useCallback(() => { pulseLoop.current = Animated.loop( Animated.sequence([ Animated.timing(pulseAnim, { toValue: 1.15, duration: 700, useNativeDriver: true, }), Animated.timing(pulseAnim, { toValue: 1, duration: 700, useNativeDriver: true, }), ]) ); pulseLoop.current.start(); Animated.timing(glowAnim, { toValue: 1, duration: 300, useNativeDriver: true, }).start(); }, [pulseAnim, glowAnim]); const stopPulse = useCallback(() => { pulseLoop.current?.stop(); pulseAnim.setValue(1); Animated.timing(glowAnim, { toValue: 0, duration: 200, useNativeDriver: true, }).start(); }, [pulseAnim, glowAnim]); const startRecording = useCallback(async () => { try { await stopPlayback(); const { granted } = await requestRecordingPermissionsAsync(); if (!granted) return; await setAudioModeAsync({ allowsRecording: true, playsInSilentMode: true, }); startPulse(); await recorder.prepareToRecordAsync(); recorder.record(); setIsRecording(true); } catch (err) { console.error("Failed to start recording:", err); stopPulse(); setIsRecording(false); } }, [recorder, startPulse, stopPulse]); const stopAndSend = useCallback(async () => { stopPulse(); setIsRecording(false); try { await recorder.stop(); // Reset audio mode for playback await setAudioModeAsync({ allowsRecording: false, playsInSilentMode: true, }); const uri = recorder.uri; if (uri) { onVoiceRecorded(uri); } } catch (err) { console.error("Failed to stop recording:", err); } }, [recorder, stopPulse, onVoiceRecorded]); const cancelRecording = useCallback(async () => { Haptics.notificationAsync(Haptics.NotificationFeedbackType.Warning); stopPulse(); setIsRecording(false); try { await recorder.stop(); } catch { // ignore } }, [recorder, stopPulse]); const handleTap = useCallback(async () => { Haptics.impactAsync(Haptics.ImpactFeedbackStyle.Medium); if (isRecording) { await stopAndSend(); } else { await startRecording(); } }, [isRecording, stopAndSend, startRecording]); const handleLongPress = useCallback(() => { if (isRecording) { cancelRecording(); } }, [isRecording, cancelRecording]); return ( {/* Outer pulse ring */} {/* Button */} {isRecording ? "⏹" : "🎤"} {/* Label */} {isRecording ? "Recording..." : "Tap to talk"} ); }