Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
81 changes: 43 additions & 38 deletions apps/mobile/src/context/AppContext.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -272,7 +272,7 @@ interface AppContextType {
dispatch: React.Dispatch<AppAction>;
actions: {
checkApiHealth: () => Promise<void>;
uploadRecording: () => Promise<void>;
uploadRecording: (recordingUri?: string) => Promise<void>;
uploadRecordingWithMode: (mode?: 'fast' | 'optimized') => Promise<void>;
resetApp: () => void;
setCoachingMode: (mode: 'fast' | 'optimized') => void;
Expand Down Expand Up @@ -339,52 +339,57 @@ export function AppProvider({ children }: { children: ReactNode }) {
}
}, []);

const uploadRecording = useCallback(async () => {
if (!state.recordingData.uri) {
dispatch({ type: 'SET_ERROR', payload: 'No recording found to upload' });
return;
}
const uploadRecording = useCallback(
async (recordingUri?: string) => {
const uriToUse = recordingUri || state.recordingData.uri;

dispatch({ type: 'START_UPLOAD' });
if (!uriToUse) {
dispatch({
type: 'SET_ERROR',
payload: 'No recording found to upload',
});
return;
}

try {
const result = await apiService.submitCheckin(state.recordingData.uri);
dispatch({ type: 'START_UPLOAD' });

if (result.success && result.data) {
// Extract processing time from success message
const processingTimeMatch = result.message?.match(/(\d+)ms/);
const processingTime = processingTimeMatch
? parseInt(processingTimeMatch[1])
: undefined;
try {
const result = await apiService.submitCheckin(uriToUse);

dispatch({
type: 'UPLOAD_SUCCESS',
payload: {
data: result.data,
processingTime,
},
});
if (result.success && result.data) {
// Extract processing time from success message
const processingTimeMatch = result.message?.match(/(\d+)ms/);
const processingTime = processingTimeMatch
? parseInt(processingTimeMatch[1])
: undefined;

// Navigate to results after successful upload
dispatch({ type: 'NAVIGATE_TO', payload: 'results' });
} else {
// Handle API errors with detailed error information
const errorMessage = result.error || 'Upload failed';
const details = result.code ? ` (${result.code})` : '';
dispatch({
type: 'UPLOAD_SUCCESS',
payload: {
data: result.data,
processingTime,
},
});
} else {
// Handle API errors with detailed error information
const errorMessage = result.error || 'Upload failed';
const details = result.code ? ` (${result.code})` : '';
dispatch({
type: 'UPLOAD_ERROR',
payload: `${errorMessage}${details}`,
});
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Upload recording error:', error);
dispatch({
type: 'UPLOAD_ERROR',
payload: `${errorMessage}${details}`,
payload: 'Network error. Please check your connection and try again.',
});
}
} catch (error) {
// eslint-disable-next-line no-console
console.error('Upload recording error:', error);
dispatch({
type: 'UPLOAD_ERROR',
payload: 'Network error. Please check your connection and try again.',
});
}
}, [state.recordingData.uri]);
},
[state.recordingData.uri]
);

const uploadRecordingWithMode = useCallback(
async (mode?: 'fast' | 'optimized') => {
Expand Down
23 changes: 15 additions & 8 deletions apps/mobile/src/hooks/useAudioRecording.ts
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,9 @@
// Check permissions first
if (!hasPermissions) {
const granted = await requestPermissions();
if (!granted) return false;
if (!granted) {
return false;
}
}

// Prepare and start recording
Expand All @@ -101,29 +103,34 @@
try {
setError(null);

if (!recorderState.isRecording) {
return null;
}

// Stop timer
// Stop timer first
if (timerRef.current) {
clearInterval(timerRef.current);
timerRef.current = null;
}

// Stop recording
await audioRecorder.stop();
// Stop recording even if state shows not recording (defensive programming)
try {
await audioRecorder.stop();
} catch (stopError) {
// Continue anyway, this might be expected if already stopped
}

// Get recording URI
const uri = audioRecorder.uri;
setRecordingUri(uri);

if (!uri) {
setError('Recording file not found after stopping');
return null;
}

return uri;
} catch (err) {
setError('Failed to stop recording');
return null;
}
}, [recorderState.isRecording, audioRecorder]);

Check warning on line 133 in apps/mobile/src/hooks/useAudioRecording.ts

View workflow job for this annotation

GitHub Actions / Lint and Test

React Hook useCallback has an unnecessary dependency: 'recorderState.isRecording'. Either exclude it or remove the dependency array

return {
isRecording: recorderState.isRecording,
Expand Down
61 changes: 38 additions & 23 deletions apps/mobile/src/screens/RecordingScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import React, { useState, useEffect, useCallback } from 'react';
import React, { useState, useEffect, useCallback, useRef } from 'react';
import {
View,
Text,
Expand All @@ -24,6 +24,7 @@ export function RecordingScreen() {
const audioRecording = useAudioRecording();
const [timeRemaining, setTimeRemaining] = useState(MAX_RECORDING_DURATION);
const [pulseAnimation] = useState(new Animated.Value(1));
const stopRecordingRef = useRef<(() => Promise<void>) | null>(null);

useEffect(() => {
// Request permissions when component mounts
Expand All @@ -46,26 +47,46 @@ export function RecordingScreen() {
},
});

// Navigate to results immediately for testing without backend
// Navigate to results immediately to show loading/processing state
dispatch({ type: 'NAVIGATE_TO', payload: 'results' });

// Still try to upload in background (will fail gracefully without backend)
setTimeout(async () => {
try {
await actions.uploadRecording();
} catch (error) {
// Silently fail - we're already on results screen
// Upload will be retried when backend is available
}
}, 500);
// Start upload process in background - this will update the results screen with data or errors
try {
await actions.uploadRecording(recordingUri);
} catch (uploadError) {
// Error will be handled by the uploadRecording function and shown in results screen
}
} else {
Alert.alert('Recording Error', 'Failed to save recording.');
Alert.alert(
'Recording Error',
'Failed to save recording. No recording file was created.'
);
// Navigate to results to show error state
dispatch({
type: 'SET_ERROR',
payload: 'Recording failed - no audio file was created',
});
dispatch({ type: 'NAVIGATE_TO', payload: 'results' });
}
} catch (error) {
Alert.alert('Recording Error', 'Failed to stop recording.');
Alert.alert(
'Recording Error',
`Failed to stop recording: ${error instanceof Error ? error.message : 'Unknown error'}`
);
// Set error state and navigate to results
dispatch({
type: 'SET_ERROR',
payload: `Recording failed: ${error instanceof Error ? error.message : 'Unknown error'}`,
});
dispatch({ type: 'NAVIGATE_TO', payload: 'results' });
}
}, [audioRecording, dispatch, actions]);

// Store the stopRecording function in a ref to avoid dependency cycles
useEffect(() => {
stopRecordingRef.current = stopRecording;
}, [stopRecording]);

useEffect(() => {
// Pulse animation for recording button
const pulseAnimationLoop = Animated.loop(
Expand Down Expand Up @@ -101,15 +122,11 @@ export function RecordingScreen() {
setTimeRemaining(Math.max(0, remaining));

// Auto-stop at max duration
if (remaining <= 0) {
stopRecording();
if (remaining <= 0 && stopRecordingRef.current) {
stopRecordingRef.current();
}
}
}, [
audioRecording.recordingDuration,
audioRecording.isRecording,
stopRecording,
]);
}, [audioRecording.recordingDuration, audioRecording.isRecording]);

useEffect(() => {
// Handle audio recording errors
Expand Down Expand Up @@ -310,9 +327,7 @@ export function RecordingScreen() {
{/* Tips - positioned at bottom */}
{!audioRecording.isRecording && (
<Card variant="default" padding="md" style={styles.tipsCard}>
<Text style={styles.tipsTitle}>
💡 Tips for a good recording:
</Text>
<Text style={styles.tipsTitle}>Tips for a good recording:</Text>
<Text style={styles.tipsText}>
• Find a quiet space{'\n'}• Speak clearly and naturally{'\n'}•
Share whatever feels comfortable{'\n'}• There&apos;s no right or
Expand Down
Loading