From c2ddc7d8e552e47c5d4d9a53456baefb56db5f17 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 26 Sep 2024 15:52:08 -0400 Subject: [PATCH 01/44] Adds functionality for asking for permissions for Audio. Adds Audio navigation and dummy screen. --- src/frontend/Navigation/Stack/AppScreens.tsx | 10 +- .../screens/Audio/CreateRecording/index.tsx | 40 +++++ src/frontend/screens/Audio/index.tsx | 31 ++++ src/frontend/sharedComponents/ActionRow.tsx | 46 +++++- .../sharedComponents/PermissionAudio.tsx | 150 +++++++++--------- src/frontend/sharedTypes/navigation.ts | 1 + 6 files changed, 195 insertions(+), 83 deletions(-) create mode 100644 src/frontend/screens/Audio/CreateRecording/index.tsx create mode 100644 src/frontend/screens/Audio/index.tsx diff --git a/src/frontend/Navigation/Stack/AppScreens.tsx b/src/frontend/Navigation/Stack/AppScreens.tsx index 52c35a0c8..d2b89e90e 100644 --- a/src/frontend/Navigation/Stack/AppScreens.tsx +++ b/src/frontend/Navigation/Stack/AppScreens.tsx @@ -76,6 +76,10 @@ import {SettingsPrivacyPolicy} from '../../screens/Settings/DataAndPrivacy/Setti import {TrackEdit} from '../../screens/TrackEdit/index.tsx'; import {Config} from '../../screens/Settings/Config'; import {HowToLeaveProject} from '../../screens/HowToLeaveProject.tsx'; +import { + Audio, + navigationOptions as audioNavigationOptions, +} from '../../screens/Audio/index.tsx'; export const TAB_BAR_HEIGHT = 70; @@ -322,7 +326,11 @@ export const createDefaultScreenGroup = ({ component={HowToLeaveProject} options={{headerShown: false}} /> - + {process.env.EXPO_PUBLIC_FEATURE_TEST_DATA_UI && ( { + navigation.setOptions({ + headerLeft: props => ( + + ), + }); + }, [navigation]); + return ( + + + Create Recording + + + ); +} + +const styles = StyleSheet.create({ + contentContainer: {flex: 1}, + container: { + flex: 1, + justifyContent: 'center', + }, + message: { + color: WHITE, + fontSize: 20, + textAlign: 'center', + }, +}); diff --git a/src/frontend/screens/Audio/index.tsx b/src/frontend/screens/Audio/index.tsx new file mode 100644 index 000000000..8549f37ed --- /dev/null +++ b/src/frontend/screens/Audio/index.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; + +import {DARK_GREY, WHITE} from '../../lib/styles'; +import {CustomHeaderLeft} from '../../sharedComponents/CustomHeaderLeft'; +import {StatusBar} from 'expo-status-bar'; +import {CreateRecording} from './CreateRecording'; + +export function Audio() { + return ( + <> + + + + ); +} + +export const navigationOptions: NativeStackNavigationOptions = { + contentStyle: {backgroundColor: DARK_GREY}, + headerTintColor: WHITE, + headerShadowVisible: false, + headerTitle: () => null, + headerStyle: {backgroundColor: 'transparent'}, + headerTransparent: true, + headerLeft: props => ( + + ), +}; diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index 41e718ff6..8c7d82ced 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -1,10 +1,14 @@ -import React from 'react'; +import React, {useCallback} from 'react'; import {defineMessages, useIntl} from 'react-intl'; import {ActionTab} from './ActionTab'; import PhotoIcon from '../images/observationEdit/Photo.svg'; +import AudioIcon from '../images/observationEdit/Audio.svg'; import DetailsIcon from '../images/observationEdit/Details.svg'; import {useNavigationFromRoot} from '../hooks/useNavigationWithTypes'; import {Preset} from '@comapeo/schema'; +import {PermissionAudio} from './PermissionAudio'; +import {Audio} from 'expo-av'; +import {useBottomSheetModal} from '../sharedComponents/BottomSheetModal'; const m = defineMessages({ audioButton: { @@ -23,23 +27,37 @@ const m = defineMessages({ description: 'Button label to add details', }, }); - interface ActionButtonsProps { fieldRefs?: Preset['fieldRefs']; } - export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { const {formatMessage: t} = useIntl(); const navigation = useNavigationFromRoot(); + const { + openSheet: openAudioPermissionSheet, + sheetRef: audioPermissionSheetRef, + closeSheet: closeAudioPermissionSheet, + isOpen: isAudioPermissionSheetOpen, + } = useBottomSheetModal({ + openOnMount: false, + }); const handleCameraPress = () => { navigation.navigate('AddPhoto'); }; - const handleDetailsPress = () => { navigation.navigate('ObservationFields', {question: 1}); }; + const handleAudioPress = useCallback(async () => { + const {status} = await Audio.getPermissionsAsync(); + if (status === 'granted') { + navigation.navigate('Audio'); + } else { + openAudioPermissionSheet(); + } + }, [navigation, openAudioPermissionSheet]); + const bottomSheetItems = [ { icon: , @@ -49,8 +67,15 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { }, ]; + if (process.env.EXPO_PUBLIC_FEATURE_AUDIO) { + bottomSheetItems.unshift({ + icon: , + label: t(m.audioButton), + onPress: handleAudioPress, + testID: 'OBS.add-audio-btn', + }); + } if (fieldRefs?.length) { - // Only show the option to add details if preset fields are defined. bottomSheetItems.push({ icon: , label: t(m.detailsButton), @@ -59,5 +84,14 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { }); } - return ; + return ( + <> + + + + ); }; diff --git a/src/frontend/sharedComponents/PermissionAudio.tsx b/src/frontend/sharedComponents/PermissionAudio.tsx index b944bd61c..500c1539a 100644 --- a/src/frontend/sharedComponents/PermissionAudio.tsx +++ b/src/frontend/sharedComponents/PermissionAudio.tsx @@ -1,72 +1,100 @@ -import React, {FC, useCallback, useEffect} from 'react'; +import React, {FC, useState, useEffect} from 'react'; import {Linking} from 'react-native'; import {defineMessages, useIntl} from 'react-intl'; -import {BottomSheetModalMethods} from '@gorhom/bottom-sheet/lib/typescript/types'; import AudioPermission from '../images/observationEdit/AudioPermission.svg'; import {BottomSheetModalContent, BottomSheetModal} from './BottomSheetModal'; import {Audio} from 'expo-av'; -import {PermissionStatus} from 'expo-av/build/Audio'; - -const handleRequestPermissions = (): void => { - Audio.requestPermissionsAsync().catch(() => {}); -}; - -const handleOpenSettings = () => { - Linking.openSettings(); -}; +import {BottomSheetModalMethods} from '@gorhom/bottom-sheet/lib/typescript/types'; +import {PermissionResponse} from 'expo-modules-core'; +import {useNavigationFromRoot} from '../hooks/useNavigationWithTypes'; -interface PermissionAudio { - sheetRef: React.RefObject; +const m = defineMessages({ + title: { + id: 'screens.AudioPermission.title', + defaultMessage: 'Recording Audio with CoMapeo', + description: 'Screen title for audio permission screen', + }, + description: { + id: 'screens.AudioPermission.description', + defaultMessage: + 'To record audio while using the app and in the background CoMapeo needs to access your microphone. Please enable microphone permissions in your app settings.', + description: 'Screen description for audio permission screen', + }, + notNowButtonText: { + id: 'screens.AudioPermission.Button.notNow', + defaultMessage: 'Not Now', + description: 'Screen button text for not allowed audio permission', + }, + allowButtonText: { + id: 'screens.AudioPermission.Button.allow', + defaultMessage: 'Allow', + description: 'Screen button text for allow the audio permission', + }, + goToSettingsButtonText: { + id: 'screens.AudioPermission.Button.goToSettings', + defaultMessage: 'Go to Settings', + description: + 'Screen button text for navigate user to settings when audio permission was denied', + }, +}); +interface PermissionAudioProps { closeSheet: () => void; isOpen: boolean; + sheetRef: React.RefObject; } -export const PermissionAudio: FC = props => { - const {sheetRef, closeSheet, isOpen} = props; +export const PermissionAudio: FC = ({ + closeSheet, + isOpen, + sheetRef, +}) => { const {formatMessage: t} = useIntl(); - const [permissionResponse] = Audio.usePermissions({request: false}); + const navigation = useNavigationFromRoot(); + const [permissionResponse, setPermissionResponse] = + useState(null); - const handlePermissionGranted = useCallback(() => { + const handleOpenSettings = () => { + Linking.openSettings(); closeSheet(); - }, [closeSheet]); - - const isPermissionGranted = Boolean(permissionResponse?.granted); + }; - useEffect(() => { - if (isPermissionGranted) handlePermissionGranted(); - }, [isPermissionGranted, handlePermissionGranted]); + const handleRequestPermission = async () => { + const response = await Audio.requestPermissionsAsync(); + setPermissionResponse(response); + if (response.status === 'granted') { + closeSheet(); + navigation.navigate('Audio'); + } else if (response.status === 'denied' && response.canAskAgain) { + closeSheet(); + } else if (response.status === 'denied' && !response.canAskAgain) { + handleOpenSettings(); + } + }; let onPressActionButton: () => void; let actionButtonText: string; - switch (permissionResponse?.status) { - case undefined: - case PermissionStatus.UNDETERMINED: - onPressActionButton = handleOpenSettings; - actionButtonText = t(m.allowButtonText); - break; - case PermissionStatus.DENIED: - if (permissionResponse.canAskAgain) { - onPressActionButton = handleOpenSettings; - actionButtonText = t(m.allowButtonText); - } else { - onPressActionButton = handleRequestPermissions; - actionButtonText = t(m.goToSettingsButtonText); - } - break; - case PermissionStatus.GRANTED: - onPressActionButton = handlePermissionGranted; - actionButtonText = t(m.allowButtonText); - break; - default: - throw new Error('Unexpected permission response'); + + if (!permissionResponse) { + onPressActionButton = async () => { + await handleRequestPermission(); + }; + actionButtonText = t(m.allowButtonText); + } else if (permissionResponse.status === 'denied') { + onPressActionButton = handleOpenSettings; + actionButtonText = t(m.goToSettingsButtonText); + } else { + onPressActionButton = async () => { + await handleRequestPermission(); + }; + actionButtonText = t(m.allowButtonText); } return ( + fullScreen> } title={t(m.title)} @@ -87,33 +115,3 @@ export const PermissionAudio: FC = props => { ); }; - -const m = defineMessages({ - title: { - id: 'screens.AudioPermission.title', - defaultMessage: 'Recording Audio with CoMapeo', - description: 'Screen title for audio permission screen', - }, - description: { - id: 'screens.AudioPermission.description', - defaultMessage: - 'To record audio while using the app and in the background CoMapeo needs to access your microphone.', - description: 'Screen description for audio permission screen', - }, - notNowButtonText: { - id: 'screens.AudioPermission.Button.notNow', - defaultMessage: 'Not Now', - description: 'Screen button text for not allowed audio permission', - }, - allowButtonText: { - id: 'screens.AudioPermission.Button.allow', - defaultMessage: 'Allow', - description: 'Screen button text for allow the audio permission', - }, - goToSettingsButtonText: { - id: 'screens.AudioPermission.Button.goToSettings', - defaultMessage: 'Go to Settings', - description: - 'Screen button text for navigate user to settings when audio permission was denied', - }, -}); diff --git a/src/frontend/sharedTypes/navigation.ts b/src/frontend/sharedTypes/navigation.ts index 82136bb73..7742e421c 100644 --- a/src/frontend/sharedTypes/navigation.ts +++ b/src/frontend/sharedTypes/navigation.ts @@ -102,6 +102,7 @@ export type RootStackParamsList = { DataAndPrivacy: undefined; SettingsPrivacyPolicy: undefined; HowToLeaveProject: undefined; + Audio: undefined; }; export type OnboardingParamsList = { From 11c6fe78ca110696e0f11337dd4e5a4d59ddc635 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 26 Sep 2024 16:15:16 -0400 Subject: [PATCH 02/44] Adds messages --- messages/en.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/messages/en.json b/messages/en.json index 5976c4616..8e6c4fe78 100644 --- a/messages/en.json +++ b/messages/en.json @@ -354,7 +354,7 @@ }, "screens.AudioPermission.description": { "description": "Screen description for audio permission screen", - "message": "To record audio while using the app and in the background CoMapeo needs to access your microphone." + "message": "To record audio while using the app and in the background CoMapeo needs to access your microphone. Please enable microphone permissions in your app settings." }, "screens.AudioPermission.title": { "description": "Screen title for audio permission screen", From ace81dd39423126e4cc9b54a2897e46af5478d5a Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 26 Sep 2024 16:35:30 -0400 Subject: [PATCH 03/44] Adds ability to create a recording. --- .../screens/Audio/AnimatedBackground.tsx | 33 +++++++ .../screens/Audio/ContentWithControls.tsx | 78 +++++++++++++++++ src/frontend/screens/Audio/Controls.tsx | 85 +++++++++++++++++++ .../Audio/CreateRecording/RecordingActive.tsx | 64 ++++++++++++++ .../Audio/CreateRecording/RecordingIdle.tsx | 46 ++++++++++ .../screens/Audio/CreateRecording/index.tsx | 58 ++++++------- .../CreateRecording/useAudioRecording.ts | 74 ++++++++++++++++ .../CreateRecording/useAutoStopRecording.ts | 12 +++ src/frontend/screens/Audio/constants.ts | 1 + 9 files changed, 419 insertions(+), 32 deletions(-) create mode 100644 src/frontend/screens/Audio/AnimatedBackground.tsx create mode 100644 src/frontend/screens/Audio/ContentWithControls.tsx create mode 100644 src/frontend/screens/Audio/Controls.tsx create mode 100644 src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx create mode 100644 src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx create mode 100644 src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts create mode 100644 src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts create mode 100644 src/frontend/screens/Audio/constants.ts diff --git a/src/frontend/screens/Audio/AnimatedBackground.tsx b/src/frontend/screens/Audio/AnimatedBackground.tsx new file mode 100644 index 000000000..07a60c10d --- /dev/null +++ b/src/frontend/screens/Audio/AnimatedBackground.tsx @@ -0,0 +1,33 @@ +import React from 'react'; +import {Dimensions, StyleSheet} from 'react-native'; +import Animated, {SharedValue, useAnimatedStyle} from 'react-native-reanimated'; +import {useSafeAreaInsets} from 'react-native-safe-area-context'; + +import {MAX_RECORDING_DURATION_MS} from './constants'; + +export function AnimatedBackground({ + elapsedTimeValue, +}: { + elapsedTimeValue: SharedValue; +}) { + const {top} = useSafeAreaInsets(); + const {height} = Dimensions.get('window'); + + const animatedStyles = useAnimatedStyle(() => ({ + height: + (height + top) * + (elapsedTimeValue.value * (1 / MAX_RECORDING_DURATION_MS)), + backgroundColor: `hsl(216, 100%, ${elapsedTimeValue.value * (1 / MAX_RECORDING_DURATION_MS) * 50}%)`, + })); + + return ; +} + +const styles = StyleSheet.create({ + fill: { + position: 'absolute', + zIndex: -1, + bottom: 0, + width: '100%', + }, +}); diff --git a/src/frontend/screens/Audio/ContentWithControls.tsx b/src/frontend/screens/Audio/ContentWithControls.tsx new file mode 100644 index 000000000..784745090 --- /dev/null +++ b/src/frontend/screens/Audio/ContentWithControls.tsx @@ -0,0 +1,78 @@ +import React, {ReactNode} from 'react'; +import {StyleSheet, View} from 'react-native'; +import {Bar} from 'react-native-progress'; +import {Duration} from 'luxon'; + +import {MEDIUM_GREY, WHITE} from '../../lib/styles'; +import {ScreenContentWithDock} from '../../sharedComponents/ScreenContentWithDock'; +import {Text} from '../../sharedComponents/Text'; + +export function ContentWithControls({ + controls, + message, + progress, + timeElapsed, +}: { + controls: ReactNode; + message?: string; + progress?: number; + timeElapsed: number; +}) { + return ( + + + + + {Duration.fromMillis(timeElapsed).toFormat('mm:ss')} + + + {typeof progress === 'number' ? ( + 0 ? progress : 0.00000001} + indeterminate={false} + width={null} + color={WHITE} + borderColor="transparent" + borderRadius={0} + borderWidth={0} + unfilledColor={MEDIUM_GREY} + /> + ) : ( + + )} + + {message} + + + ); +} + +const styles = StyleSheet.create({ + contentContainer: {flex: 1}, + dockContainer: {paddingVertical: 24}, + container: { + flex: 1, + justifyContent: 'flex-end', + }, + timerContainer: { + flex: 1, + justifyContent: 'center', + gap: 48, + }, + message: { + color: WHITE, + fontSize: 20, + textAlign: 'center', + }, + timerText: { + fontFamily: 'Rubik', + fontSize: 96, + fontWeight: 'bold', + color: WHITE, + textAlign: 'center', + }, +}); diff --git a/src/frontend/screens/Audio/Controls.tsx b/src/frontend/screens/Audio/Controls.tsx new file mode 100644 index 000000000..9a8989f0d --- /dev/null +++ b/src/frontend/screens/Audio/Controls.tsx @@ -0,0 +1,85 @@ +import React, {PropsWithChildren} from 'react'; +import {Pressable, PressableProps, StyleSheet, View} from 'react-native'; + +import {MAGENTA, BLACK, LIGHT_GREY, WHITE} from '../../lib/styles'; + +type BaseProps = PropsWithChildren; + +function ControlButtonPrimaryBase({children, ...pressableProps}: BaseProps) { + return ( + [ + styles.basePressable, + typeof pressableProps.style === 'function' + ? pressableProps.style({pressed}) + : pressableProps.style, + pressed && styles.pressablePressed, + ]}> + {children} + + ); +} + +export function Record(props: BaseProps) { + return ( + + + + ); +} + +export function Stop(props: BaseProps) { + return ( + + + + ); +} + +export function Row({children}: PropsWithChildren) { + return {children}; +} + +const PRIMARY_CONTROL_DIAMETER = 96; + +const styles = StyleSheet.create({ + basePressable: { + height: PRIMARY_CONTROL_DIAMETER, + width: PRIMARY_CONTROL_DIAMETER, + borderRadius: PRIMARY_CONTROL_DIAMETER, + borderWidth: 12, + borderColor: WHITE, + overflow: 'hidden', + backgroundColor: WHITE, + justifyContent: 'center', + }, + pressablePressed: { + backgroundColor: LIGHT_GREY, + borderColor: LIGHT_GREY, + }, + + record: { + height: PRIMARY_CONTROL_DIAMETER, + backgroundColor: MAGENTA, + }, + stop: { + height: PRIMARY_CONTROL_DIAMETER / 3, + width: PRIMARY_CONTROL_DIAMETER / 3, + backgroundColor: BLACK, + alignSelf: 'center', + }, + play: { + justifyContent: 'center', + alignItems: 'center', + }, + + controlsRow: { + flexDirection: 'row', + justifyContent: 'center', + alignItems: 'center', + }, + sideControl: { + position: 'absolute', + }, +}); diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx new file mode 100644 index 000000000..9d3d22351 --- /dev/null +++ b/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx @@ -0,0 +1,64 @@ +import React, {useEffect} from 'react'; +import {defineMessages, useIntl} from 'react-intl'; +import {useDerivedValue, withTiming} from 'react-native-reanimated'; + +import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; +import {AnimatedBackground} from '../AnimatedBackground'; +import {ContentWithControls} from '../ContentWithControls'; +import * as Controls from '../Controls'; +import {MAX_RECORDING_DURATION_MS} from '../constants'; +import {useAutoStopRecording} from './useAutoStopRecording'; + +const m = defineMessages({ + description: { + id: 'screens.AudioScreen.CreateRecording.RecordingActive.description', + defaultMessage: + 'Less than {length} {length, plural, one {minute} other {minutes}} left', + }, +}); + +export function RecordingActive({ + duration, + onPressStop, +}: { + duration: number; + onPressStop: () => void; +}) { + const navigation = useNavigationFromRoot(); + const {formatMessage: t} = useIntl(); + + const minutesRemaining = Math.ceil( + (MAX_RECORDING_DURATION_MS - duration) / 60_000, + ); + + const elapsedTimeValue = useDerivedValue(() => { + return withTiming(duration, {duration: 500}); + }, [duration]); + + useEffect(() => { + navigation.setOptions({headerLeft: () => null}); + }, [navigation]); + + useAutoStopRecording(minutesRemaining, onPressStop); + + return ( + <> + 0 + ? t(m.description, { + length: minutesRemaining, + }) + : undefined + } + controls={ + + + + } + /> + + + ); +} diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx new file mode 100644 index 000000000..d41adadb6 --- /dev/null +++ b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx @@ -0,0 +1,46 @@ +import React, {useEffect} from 'react'; +import {defineMessages, useIntl} from 'react-intl'; + +import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; +import {CustomHeaderLeft} from '../../../sharedComponents/CustomHeaderLeft'; +import {ContentWithControls} from '../ContentWithControls'; +import * as Controls from '../Controls'; +import {MAX_RECORDING_DURATION_MS} from '../constants'; + +const m = defineMessages({ + description: { + id: 'screens.AudioScreen.CreateRecording.RecordingIdle.description', + defaultMessage: + 'Record up to {length} {length, plural, one {minute} other {minutes}}', + }, +}); + +export function RecordingIdle({onPressRecord}: {onPressRecord: () => void}) { + const navigation = useNavigationFromRoot(); + const {formatMessage: t} = useIntl(); + + useEffect(() => { + navigation.setOptions({ + headerLeft: props => ( + + ), + }); + }, [navigation]); + + return ( + + + + } + /> + ); +} diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index a0aa360a5..b5d9167d7 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -1,40 +1,34 @@ import React, {useEffect} from 'react'; -import {Text, View, StyleSheet} from 'react-native'; -import {WHITE} from '../../../lib/styles'; - import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; -import {CustomHeaderLeft} from '../../../sharedComponents/CustomHeaderLeft'; +import {RecordingActive} from './RecordingActive'; +import {RecordingIdle} from './RecordingIdle'; +import {useAudioRecording} from './useAudioRecording'; export function CreateRecording() { const navigation = useNavigationFromRoot(); + const recordingState = useAudioRecording(); + useEffect(() => { - navigation.setOptions({ - headerLeft: props => ( - - ), + const unsubscribe = navigation.addListener('focus', () => { + recordingState.reset().catch(error => { + console.error('Error resetting recording:', error); + }); }); - }, [navigation]); - return ( - - - Create Recording - - - ); -} -const styles = StyleSheet.create({ - contentContainer: {flex: 1}, - container: { - flex: 1, - justifyContent: 'center', - }, - message: { - color: WHITE, - fontSize: 20, - textAlign: 'center', - }, -}); + return unsubscribe; + }, [navigation, recordingState]); + + switch (recordingState.status) { + case 'idle': { + return ; + } + case 'active': { + return ( + + ); + } + } +} diff --git a/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts new file mode 100644 index 000000000..db12a0b92 --- /dev/null +++ b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts @@ -0,0 +1,74 @@ +import {useState} from 'react'; +import {Audio} from 'expo-av'; + +type AudioRecordingIdle = { + status: 'idle'; + startRecording: () => Promise; + reset: () => Promise; +}; + +type AudioRecordingActive = { + status: 'active'; + /** + * Time elapsed in milliseconds + */ + duration: number; + stopRecording: () => Promise; + reset: () => Promise; +}; + +type AudioRecordingState = AudioRecordingIdle | AudioRecordingActive; + +export function useAudioRecording(): AudioRecordingState { + const [state, setState] = useState<{ + recording: Audio.Recording; + status: Audio.RecordingStatus; + } | null>(null); + + const reset = async () => { + if (state) { + if (state.status.isRecording) { + await state.recording.stopAndUnloadAsync(); + } + } + setState(null); + }; + + if (!state) { + return { + status: 'idle', + startRecording: async () => { + const {recording, status} = await Audio.Recording.createAsync( + Audio.RecordingOptionsPresets.HIGH_QUALITY, + status => { + setState(prev => { + if (!prev) return prev; + return { + ...prev, + status, + }; + }); + }, + 1000, + ); + + setState({recording, status}); + }, + reset, + }; + } + + return { + status: 'active', + duration: state.status.durationMillis, + stopRecording: async () => { + const status = await state.recording.stopAndUnloadAsync(); + + setState(prev => { + if (!prev) return prev; + return {...prev, status}; + }); + }, + reset, + }; +} diff --git a/src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts b/src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts new file mode 100644 index 000000000..cdcf55d40 --- /dev/null +++ b/src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts @@ -0,0 +1,12 @@ +import {useEffect} from 'react'; + +export function useAutoStopRecording( + minutesRemaining: number, + onPressStop: () => void, +) { + useEffect(() => { + if (minutesRemaining === 0) { + onPressStop(); + } + }, [minutesRemaining, onPressStop]); +} diff --git a/src/frontend/screens/Audio/constants.ts b/src/frontend/screens/Audio/constants.ts new file mode 100644 index 000000000..6462d61b0 --- /dev/null +++ b/src/frontend/screens/Audio/constants.ts @@ -0,0 +1 @@ +export const MAX_RECORDING_DURATION_MS = 5 * 60_000; From 42f40d0f1c4fd318e891e1ff20dffeb10ac57cf6 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 26 Sep 2024 16:49:53 -0400 Subject: [PATCH 04/44] Adds messages. --- messages/en.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/messages/en.json b/messages/en.json index 8e6c4fe78..969332023 100644 --- a/messages/en.json +++ b/messages/en.json @@ -360,6 +360,12 @@ "description": "Screen title for audio permission screen", "message": "Recording Audio with CoMapeo" }, + "screens.AudioScreen.CreateRecording.RecordingActive.description": { + "message": "Less than {length} {length, plural, one {minute} other {minutes}} left" + }, + "screens.AudioScreen.CreateRecording.RecordingIdle.description": { + "message": "Record up to {length} {length, plural, one {minute} other {minutes}}" + }, "screens.CameraScreen.goToSettings": { "message": "Go to Settings" }, From 7491659ee7f09ab49a445d5ff2e09a343783cc2b Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 26 Sep 2024 17:07:25 -0400 Subject: [PATCH 05/44] Adds ability to playback and save audio recordings. --- .../usePersistedDraftObservation/index.ts | 13 +- src/frontend/hooks/server/media.ts | 22 +++ src/frontend/hooks/useDraftObservation.ts | 10 ++ src/frontend/lib/file-system.ts | 10 +- src/frontend/screens/Audio/Controls.tsx | 36 ++++- .../Audio/CreateRecording/RecordingDone.tsx | 149 ++++++++++++++++++ .../screens/Audio/CreateRecording/index.tsx | 19 +++ .../CreateRecording/useAudioRecording.ts | 47 +++++- src/frontend/screens/Audio/Playback.tsx | 57 +++++++ .../screens/Audio/RecordingSuccessModal.tsx | 113 +++++++++++++ .../screens/Audio/useAudioPlayback.ts | 80 ++++++++++ .../screens/ObservationCreate/index.tsx | 45 ++++-- src/frontend/sharedComponents/Button.tsx | 4 +- src/frontend/sharedTypes/index.ts | 5 + 14 files changed, 590 insertions(+), 20 deletions(-) create mode 100644 src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx create mode 100644 src/frontend/screens/Audio/Playback.tsx create mode 100644 src/frontend/screens/Audio/RecordingSuccessModal.tsx create mode 100644 src/frontend/screens/Audio/useAudioPlayback.ts diff --git a/src/frontend/hooks/persistedState/usePersistedDraftObservation/index.ts b/src/frontend/hooks/persistedState/usePersistedDraftObservation/index.ts index 24a477cda..c038a4424 100644 --- a/src/frontend/hooks/persistedState/usePersistedDraftObservation/index.ts +++ b/src/frontend/hooks/persistedState/usePersistedDraftObservation/index.ts @@ -6,7 +6,11 @@ import { SavedPhoto, } from '../../../contexts/PhotoPromiseContext/types'; import {deletePhoto, replaceDraftPhotos} from './photosMethods'; -import {ClientGeneratedObservation, Position} from '../../../sharedTypes'; +import { + ClientGeneratedObservation, + Position, + AudioRecording, +} from '../../../sharedTypes'; import {Observation, Preset} from '@comapeo/schema'; import {usePresetsQuery} from '../../server/presets'; import {matchPreset} from '../../../lib/utils'; @@ -23,7 +27,7 @@ const emptyObservation: ClientGeneratedObservation = { export type DraftObservationSlice = { photos: Photo[]; - audioRecordings: []; + audioRecordings: AudioRecording[]; value: Observation | null | ClientGeneratedObservation; observationId?: string; actions: { @@ -53,6 +57,7 @@ export type DraftObservationSlice = { ) => void; updateTags: (tagKey: string, value: Observation['tags'][0]) => void; updatePreset: (preset: Preset) => void; + addAudioRecording: (audioRecording: AudioRecording) => void; }; }; @@ -173,6 +178,10 @@ const draftObservationSlice: StateCreator = ( }, }); }, + addAudioRecording: recording => + set({ + audioRecordings: [...get().audioRecordings, recording], + }), }, }); diff --git a/src/frontend/hooks/server/media.ts b/src/frontend/hooks/server/media.ts index 6fe1980ee..7ead94048 100644 --- a/src/frontend/hooks/server/media.ts +++ b/src/frontend/hooks/server/media.ts @@ -7,6 +7,7 @@ import {useActiveProject} from '../../contexts/ActiveProjectContext'; import {ProcessedDraftPhoto} from '../../contexts/PhotoPromiseContext/types'; import type {MapeoProjectApi} from '@comapeo/ipc'; import {ClientApi} from 'rpc-reflector'; +import {AudioRecording} from '../../sharedTypes'; export function useCreateBlobMutation(opts: {retry?: number} = {}) { const {projectApi} = useActiveProject(); @@ -34,6 +35,27 @@ export function useCreateBlobMutation(opts: {retry?: number} = {}) { }); } +export function useCreateAudioBlobMutation(opts: {retry?: number} = {}) { + const {projectApi} = useActiveProject(); + + return useMutation({ + retry: opts.retry, + mutationFn: async (audio: AudioRecording) => { + const {uri, createdAt} = audio; + + return projectApi.$blobs.create( + { + original: new URL(uri).pathname, + }, + { + mimeType: 'audio/mp4', + timestamp: createdAt, + }, + ); + }, + }); +} + const resolveAttachmentUrlQueryOptions = ( projectId: string, projectApi: ClientApi, diff --git a/src/frontend/hooks/useDraftObservation.ts b/src/frontend/hooks/useDraftObservation.ts index 084be8b71..f19765d68 100644 --- a/src/frontend/hooks/useDraftObservation.ts +++ b/src/frontend/hooks/useDraftObservation.ts @@ -8,6 +8,7 @@ import { PhotoPromiseWithMetadata, UnprocessedDraftPhoto, } from '../contexts/PhotoPromiseContext/types'; +import {AudioRecording} from '../sharedTypes'; // react native does not have a random bytes generator, `non-secure` does not require a random bytes generator. import {nanoid} from 'nanoid/non-secure'; import * as Sentry from '@sentry/react-native'; @@ -29,6 +30,7 @@ export const useDraftObservation = () => { updateTags, updatePreset, existingObservationToDraft, + addAudioRecording, } = _usePersistedDraftObservationActions(); const addPhoto = useCallback( @@ -92,6 +94,13 @@ export const useDraftObservation = () => { [deletePersistedPhoto, deletePhotoPromise], ); + const addAudio = useCallback( + (audioRecording: AudioRecording) => { + addAudioRecording(audioRecording); + }, + [addAudioRecording], + ); + return { addPhoto, clearDraft, @@ -102,5 +111,6 @@ export const useDraftObservation = () => { updatePreset, usePreset, existingObservationToDraft, + addAudio, }; }; diff --git a/src/frontend/lib/file-system.ts b/src/frontend/lib/file-system.ts index c05faf278..7d6760634 100644 --- a/src/frontend/lib/file-system.ts +++ b/src/frontend/lib/file-system.ts @@ -1,7 +1,15 @@ -import {ExternalDirectoryPath} from '@dr.pogodin/react-native-fs'; +import { + ExternalDirectoryPath, + unlink as rnUnlink, +} from '@dr.pogodin/react-native-fs'; export function convertFileUriToPosixPath(fileUri: string) { return fileUri.replace(/^file:\/\//, ''); } +export async function unlink(fileUri: string): Promise { + const posixPath = convertFileUriToPosixPath(fileUri); + return rnUnlink(posixPath); +} + export {ExternalDirectoryPath as EXTERNAL_FILES_DIR}; diff --git a/src/frontend/screens/Audio/Controls.tsx b/src/frontend/screens/Audio/Controls.tsx index 9a8989f0d..4cb63471c 100644 --- a/src/frontend/screens/Audio/Controls.tsx +++ b/src/frontend/screens/Audio/Controls.tsx @@ -1,6 +1,13 @@ import React, {PropsWithChildren} from 'react'; -import {Pressable, PressableProps, StyleSheet, View} from 'react-native'; +import { + Dimensions, + Pressable, + PressableProps, + StyleSheet, + View, +} from 'react-native'; +import PlayArrow from '../../images/PlayArrow.svg'; import {MAGENTA, BLACK, LIGHT_GREY, WHITE} from '../../lib/styles'; type BaseProps = PropsWithChildren; @@ -37,6 +44,33 @@ export function Stop(props: BaseProps) { ); } +export function Play(props: BaseProps) { + return ( + + + + + + ); +} + +export function Side({ + children, + side, +}: PropsWithChildren<{side: 'right' | 'left'}>) { + const {width} = Dimensions.get('window'); + + const midpoint = width / 2; + + const sideControlOffset = Math.max(midpoint - 200, midpoint / 3); + + return ( + + {children} + + ); +} + export function Row({children}: PropsWithChildren) { return {children}; } diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx new file mode 100644 index 000000000..5828e9e30 --- /dev/null +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -0,0 +1,149 @@ +import React, {useEffect} from 'react'; +import {HeaderBackButton} from '@react-navigation/elements'; +import {defineMessages, useIntl} from 'react-intl'; +import {Pressable} from 'react-native'; +import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; +import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; +import {WHITE} from '../../../lib/styles'; +import { + BottomSheetModalContent, + BottomSheetModal, + useBottomSheetModal, +} from '../../../sharedComponents/BottomSheetModal'; +import {CloseIcon, DeleteIcon} from '../../../sharedComponents/icons'; +import ErrorIcon from '../../../images/Error.svg'; +import {Playback} from '../Playback'; +import {RecordingSuccessModal} from '../RecordingSuccessModal'; +import {useDraftObservation} from '../../../hooks/useDraftObservation'; + +const m = defineMessages({ + deleteBottomSheetTitle: { + id: 'screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetTitle', + defaultMessage: 'Delete?', + }, + deleteBottomSheetDescription: { + id: 'screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetDescription', + defaultMessage: + 'Your Audio Recording will be permanently deleted. This cannot be undone.', + }, + deleteBottomSheetPrimaryButtonText: { + id: 'screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetPrimaryButtonText', + defaultMessage: 'Delete', + }, + deleteBottomSheetSecondaryButtonText: { + id: 'screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetSecondaryButtonText', + defaultMessage: 'Cancel', + }, +}); + +export function RecordingDone({ + createdAt, + duration, + uri, + onDelete, + onRecordAnother, +}: { + createdAt: number; + duration: number; + uri: string; + onDelete: () => void; + onRecordAnother: () => void; +}) { + const {formatMessage: t} = useIntl(); + const navigation = useNavigationFromRoot(); + const {addAudio} = useDraftObservation(); + const { + sheetRef: deleteSheetRef, + isOpen: isDeleteSheetOpen, + openSheet: openDeleteSheet, + closeSheet: closeDeleteSheet, + } = useBottomSheetModal({ + openOnMount: false, + }); + + const { + sheetRef: successSheetRef, + isOpen: isSuccessSheetOpen, + openSheet: openSuccessSheet, + closeSheet: closeSuccessSheet, + } = useBottomSheetModal({ + openOnMount: false, + }); + + useEffect(() => { + navigation.setOptions({ + headerShown: true, + headerLeft: props => ( + { + const audioRecording = { + createdAt, + duration, + uri, + }; + addAudio(audioRecording); + openSuccessSheet(); + }} + backImage={props => } + /> + ), + }); + }, [navigation, openSuccessSheet]); + + const handleReturnToEditor = () => { + closeSuccessSheet(); + navigation.navigate('ObservationCreate'); + }; + + const handleRecordAnother = async () => { + closeSuccessSheet(); + await onRecordAnother(); + navigation.navigate('Audio'); + }; + + return ( + <> + + + + } + /> + + } + title={t(m.deleteBottomSheetTitle)} + description={t(m.deleteBottomSheetDescription)} + buttonConfigs={[ + { + dangerous: true, + text: t(m.deleteBottomSheetPrimaryButtonText), + icon: , + onPress: () => { + closeDeleteSheet(); + onDelete(); + }, + variation: 'filled', + }, + { + variation: 'outlined', + text: t(m.deleteBottomSheetSecondaryButtonText), + onPress: () => { + closeDeleteSheet(); + }, + }, + ]} + /> + + + + ); +} diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index b5d9167d7..bc735f514 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -1,6 +1,7 @@ import React, {useEffect} from 'react'; import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; import {RecordingActive} from './RecordingActive'; +import {RecordingDone} from './RecordingDone'; import {RecordingIdle} from './RecordingIdle'; import {useAudioRecording} from './useAudioRecording'; @@ -30,5 +31,23 @@ export function CreateRecording() { /> ); } + case 'done': { + return ( + { + await recordingState.deleteRecording().catch(err => { + console.log(err); + }); + navigation.goBack(); + }} + onRecordAnother={async () => { + await recordingState.reset(); + }} + /> + ); + } } } diff --git a/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts index db12a0b92..a383fe4cf 100644 --- a/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts +++ b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts @@ -1,5 +1,6 @@ import {useState} from 'react'; import {Audio} from 'expo-av'; +import {unlink} from '../../../lib/file-system'; type AudioRecordingIdle = { status: 'idle'; @@ -13,16 +14,35 @@ type AudioRecordingActive = { * Time elapsed in milliseconds */ duration: number; + uri: string; + createdAt: number; stopRecording: () => Promise; reset: () => Promise; }; -type AudioRecordingState = AudioRecordingIdle | AudioRecordingActive; +type AudioRecordingDone = { + status: 'done'; + /** + * Time elapsed in milliseconds + */ + duration: number; + uri: string; + createdAt: number; + deleteRecording: () => Promise; + reset: () => Promise; +}; + +type AudioRecordingState = + | AudioRecordingIdle + | AudioRecordingActive + | AudioRecordingDone; export function useAudioRecording(): AudioRecordingState { const [state, setState] = useState<{ + createdAt: number; recording: Audio.Recording; status: Audio.RecordingStatus; + uri: string; } | null>(null); const reset = async () => { @@ -38,6 +58,7 @@ export function useAudioRecording(): AudioRecordingState { return { status: 'idle', startRecording: async () => { + const createdAt = Date.now(); const {recording, status} = await Audio.Recording.createAsync( Audio.RecordingOptionsPresets.HIGH_QUALITY, status => { @@ -52,7 +73,27 @@ export function useAudioRecording(): AudioRecordingState { 1000, ); - setState({recording, status}); + const uri = recording.getURI(); + + // Should not happen + if (uri === null) { + throw new Error('Could not get URI for recording'); + } + + setState({createdAt, recording, status, uri}); + }, + reset, + }; + } + + if (state.status.isDoneRecording) { + return { + status: 'done', + duration: state.status.durationMillis, + uri: state.uri, + createdAt: state.createdAt, + deleteRecording: async () => { + return unlink(state.uri); }, reset, }; @@ -61,6 +102,8 @@ export function useAudioRecording(): AudioRecordingState { return { status: 'active', duration: state.status.durationMillis, + uri: state.uri, + createdAt: state.createdAt, stopRecording: async () => { const status = await state.recording.stopAndUnloadAsync(); diff --git a/src/frontend/screens/Audio/Playback.tsx b/src/frontend/screens/Audio/Playback.tsx new file mode 100644 index 000000000..8b27bdf5f --- /dev/null +++ b/src/frontend/screens/Audio/Playback.tsx @@ -0,0 +1,57 @@ +import React, {ReactNode} from 'react'; +import {Duration} from 'luxon'; +import {defineMessages, useIntl} from 'react-intl'; +import {ContentWithControls} from './ContentWithControls'; +import * as Controls from './Controls'; +import {useAudioPlayback} from './useAudioPlayback'; +const m = defineMessages({ + description: { + id: 'screens.AudioScreen.Playback.description', + defaultMessage: 'Total length: {length}', + }, +}); +export function Playback({ + uri, + leftControl, + rightControl, +}: { + uri: string; + leftControl?: ReactNode; + rightControl?: ReactNode; +}) { + const {formatMessage: t} = useIntl(); + const {duration, currentPosition, isPlaying, stopPlayback, startPlayback} = + useAudioPlayback(uri); + return ( + + {leftControl ? ( + {leftControl} + ) : null} + {isPlaying ? ( + { + stopPlayback(); + }} + /> + ) : ( + { + startPlayback(); + }} + /> + )} + {rightControl ? ( + {rightControl} + ) : null} + + } + /> + ); +} diff --git a/src/frontend/screens/Audio/RecordingSuccessModal.tsx b/src/frontend/screens/Audio/RecordingSuccessModal.tsx new file mode 100644 index 000000000..196502f50 --- /dev/null +++ b/src/frontend/screens/Audio/RecordingSuccessModal.tsx @@ -0,0 +1,113 @@ +import {BottomSheetModalMethods} from '@gorhom/bottom-sheet/lib/typescript/types'; +import React, {FC} from 'react'; +import {BottomSheetModal} from '../../sharedComponents/BottomSheetModal'; +import {defineMessages, FormattedMessage, useIntl} from 'react-intl'; +import {StyleSheet, Text, View} from 'react-native'; +import SuccessIcon from '../../images/GreenCheck.svg'; +import {Button} from '../../sharedComponents/Button'; + +const m = defineMessages({ + successTitle: { + id: 'AudioPlaybackScreen.DeleteAudioRecordingModal.successTitle', + defaultMessage: 'Success!', + }, + successDescription: { + id: 'AudioPlaybackScreen.DeleteAudioRecordingModal.successDescription', + defaultMessage: 'Your {audioRecording} was added.', + }, + returnToEditorButtonText: { + id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.returnToEditor', + defaultMessage: 'Return to Editor', + }, + recordAnotherButtonText: { + id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.recordAnother', + defaultMessage: 'Record Another', + }, + audioRecording: { + id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.audioRecording', + defaultMessage: 'Audio Recording', + }, +}); + +interface RecordingSuccessModal { + sheetRef: React.RefObject; + isOpen: boolean; + onReturnToEditor: () => void; + onRecordAnother: () => void; +} + +export const RecordingSuccessModal: FC = ({ + sheetRef, + isOpen, + onReturnToEditor, + onRecordAnother, +}) => { + const {formatMessage} = useIntl(); + + return ( + + + + + {formatMessage(m.successTitle)} + + {message}, + }} + /> + + + + + + + + + ); +}; + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'center', + height: '100%', + padding: 20, + paddingTop: 80, + }, + wrapper: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + title: { + fontWeight: 'bold', + fontSize: 24, + marginTop: 10, + }, + description: {fontSize: 16, marginTop: 40}, + textBold: { + fontWeight: 'bold', + fontSize: 16, + }, +}); diff --git a/src/frontend/screens/Audio/useAudioPlayback.ts b/src/frontend/screens/Audio/useAudioPlayback.ts new file mode 100644 index 000000000..deac3691c --- /dev/null +++ b/src/frontend/screens/Audio/useAudioPlayback.ts @@ -0,0 +1,80 @@ +import {Audio, AVPlaybackStatus, AVPlaybackStatusSuccess} from 'expo-av'; +import {useCallback, useEffect, useState} from 'react'; +import {Sound} from 'expo-av/build/Audio/Sound'; +export const useAudioPlayback = (recordingUri: string) => { + const [recordedSound, setRecordedSound] = useState(null); + const [isPlaying, setPlaying] = useState(false); + const [duration, setDuration] = useState(0); + const [currentPosition, setCurrentPosition] = useState(0); + const [isReady, setReady] = useState(false); + const audioCallbackHandler = useCallback((status: AVPlaybackStatus) => { + const update = status as AVPlaybackStatusSuccess; + if (update.didJustFinish) { + setPlaying(false); + setCurrentPosition(0); + } else { + setPlaying(update.isPlaying); + if (update.isPlaying) { + setCurrentPosition(update.positionMillis); + } + } + }, []); + useEffect(() => { + Audio.Sound.createAsync({ + uri: recordingUri, + }).then(({sound, status}) => { + if ('error' in status && status.error) { + console.error('error while creating audio playback', status.error); + return; + } + const successStatus = status as AVPlaybackStatusSuccess; + setRecordedSound(sound); + setDuration(successStatus.durationMillis!); + setReady(true); + sound.setOnPlaybackStatusUpdate(audioCallbackHandler); + }); + return () => { + if (recordedSound) { + recordedSound.unloadAsync(); + } + }; + }, [audioCallbackHandler, duration, recordingUri, recordedSound]); + const startPlayback = async () => { + if (!isReady) { + console.warn( + 'startPlayback from useAudioPlayback called while recording is not ready', + ); + return; + } + if (isPlaying) { + console.warn( + 'startPlayback from useAudioPlayback called while player is already in playing state', + ); + return; + } + await recordedSound!.replayAsync(); + }; + const stopPlayback = async () => { + if (!isReady) { + console.warn( + 'stopPlayback from useAudioPlayback called while recording is not ready', + ); + return; + } + if (!isPlaying) { + console.warn( + 'stopPlayback from useAudioPlayback called while player is not in playing state', + ); + return; + } + await recordedSound!.stopAsync(); + }; + return { + duration, + isReady, + isPlaying, + currentPosition, + startPlayback, + stopPlayback, + }; +}; diff --git a/src/frontend/screens/ObservationCreate/index.tsx b/src/frontend/screens/ObservationCreate/index.tsx index 45b55bf97..fce840fbb 100644 --- a/src/frontend/screens/ObservationCreate/index.tsx +++ b/src/frontend/screens/ObservationCreate/index.tsx @@ -7,7 +7,10 @@ import {usePersistedDraftObservation} from '../../hooks/persistedState/usePersis import {NativeRootNavigationProps} from '../../sharedTypes/navigation'; import {useCreateObservation} from '../../hooks/server/observations'; import {CommonActions} from '@react-navigation/native'; -import {useCreateBlobMutation} from '../../hooks/server/media'; +import { + useCreateBlobMutation, + useCreateAudioBlobMutation, +} from '../../hooks/server/media'; import {usePersistedTrack} from '../../hooks/persistedState/usePersistedTrack'; import {SaveButton} from '../../sharedComponents/SaveButton'; import {useMostAccurateLocationForObservation} from './useMostAccurateLocationForObservation'; @@ -86,10 +89,14 @@ export const ObservationCreate = ({ const {usePreset} = useDraftObservation(); const preset = usePreset(); const value = usePersistedDraftObservation(store => store.value); + const audioRecordings = usePersistedDraftObservation( + store => store.audioRecordings, + ); const {updateTags, clearDraft} = useDraftObservation(); const photos = usePersistedDraftObservation(store => store.photos); const createObservationMutation = useCreateObservation(); const createBlobMutation = useCreateBlobMutation(); + const createAudioBlobMutation = useCreateAudioBlobMutation(); const isTracking = usePersistedTrack(state => state.isTracking); const addNewTrackLocations = usePersistedTrack( state => state.addNewLocations, @@ -123,8 +130,9 @@ export const ObservationCreate = ({ if (!value) throw new Error('no observation saved in persisted state '); const savablePhotos = photos.filter(photo => photo.type === 'processed'); + const savableAudioRecordings = audioRecordings; - if (!savablePhotos) { + if (savablePhotos.length === 0 && savableAudioRecordings.length === 0) { createObservationMutation.mutate( {value}, { @@ -151,14 +159,18 @@ export const ObservationCreate = ({ // The alternative is to save the observation but excluding photos that failed to save, which is prone to an odd UX of an observation "missing" some attachments. // This could potentially be alleviated by a more granular and informative UI about the photo-saving state, but currently there is nothing in place. // Basically, which is worse: orphaned attachments or saving observations that seem to be missing attachments? - Promise.all( - savablePhotos.map(photo => { - return createBlobMutation.mutateAsync( - // @ts-expect-error Due to TS array filtering limitations. Fixed in TS 5.5 - photo, - ); - }), - ).then(results => { + const photoPromises = savablePhotos.map(photo => { + return createBlobMutation.mutateAsync( + // @ts-expect-error Due to TS array filtering limitations. Fixed in TS 5.5 + photo, + ); + }); + + const audioPromises = savableAudioRecordings.map(audio => { + return createAudioBlobMutation.mutateAsync(audio); + }); + + Promise.all([...photoPromises, ...audioPromises]).then(results => { const newAttachments = results.map( ({driveId: driveDiscoveryId, type, name, hash}) => ({ driveDiscoveryId, @@ -203,10 +215,12 @@ export const ObservationCreate = ({ addNewTrackObservation, clearDraft, createBlobMutation, + createAudioBlobMutation, createObservationMutation, isTracking, navigation, photos, + audioRecordings, value, ]); @@ -273,7 +287,9 @@ export const ObservationCreate = ({ ), @@ -311,10 +327,15 @@ export const ObservationCreate = ({ actionsRow={} /> { createObservationMutation.reset(); createBlobMutation.reset(); + createAudioBlobMutation.reset(); }} tryAgain={createObservation} /> diff --git a/src/frontend/sharedComponents/Button.tsx b/src/frontend/sharedComponents/Button.tsx index 723c20397..eedd9c21e 100644 --- a/src/frontend/sharedComponents/Button.tsx +++ b/src/frontend/sharedComponents/Button.tsx @@ -13,7 +13,7 @@ type Size = 'medium' | 'large'; interface SharedTouchableProps { disabled?: boolean; - onPress: (event: GestureResponderEvent) => void; + onPress: (event: GestureResponderEvent) => void | Promise; } interface Props { @@ -22,7 +22,7 @@ interface Props { color?: ColorScheme; disabled?: boolean; fullWidth?: boolean; - onPress: (event: GestureResponderEvent) => void; + onPress: (event: GestureResponderEvent) => void | Promise; size?: Size; style?: ViewStyleProp; testID?: string; diff --git a/src/frontend/sharedTypes/index.ts b/src/frontend/sharedTypes/index.ts index b4ccbe4a9..9e022bb5c 100644 --- a/src/frontend/sharedTypes/index.ts +++ b/src/frontend/sharedTypes/index.ts @@ -32,6 +32,11 @@ export type PhotoVariant = 'original' | 'thumbnail' | 'preview'; export type CoordinateFormat = 'utm' | 'dd' | 'dms'; export type MediaSyncSetting = 'previews' | 'everything'; +export type AudioRecording = { + createdAt: number; + duration: number; + uri: string; +}; // Copied from @comapeo/core/src/roles.js. Created an issue to eventually expose this: https://github.com/digidem/mapeo-core-next/issues/532 export const CREATOR_ROLE_ID = 'a12a6702b93bd7ff'; From 1e41c3aae0e70a62aca23fa77f09e3b1b931f249 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 26 Sep 2024 17:08:32 -0400 Subject: [PATCH 06/44] Adds messages. --- messages/en.json | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/messages/en.json b/messages/en.json index 969332023..6f85266b0 100644 --- a/messages/en.json +++ b/messages/en.json @@ -25,6 +25,21 @@ "description": "Title of dialog that shows when cancelling a new observation", "message": "Discard observation?" }, + "AudioPlaybackScreen.DeleteAudioRecordingModal.successDescription": { + "message": "Your {audioRecording} was added." + }, + "AudioPlaybackScreen.DeleteAudioRecordingModal.successTitle": { + "message": "Success!" + }, + "AudioPlaybackScreen.SuccessAudioRecordingModal.audioRecording": { + "message": "Audio Recording" + }, + "AudioPlaybackScreen.SuccessAudioRecordingModal.recordAnother": { + "message": "Record Another" + }, + "AudioPlaybackScreen.SuccessAudioRecordingModal.returnToEditor": { + "message": "Return to Editor" + }, "Modal.GPSDisable.button": { "message": "Enable" }, @@ -363,9 +378,24 @@ "screens.AudioScreen.CreateRecording.RecordingActive.description": { "message": "Less than {length} {length, plural, one {minute} other {minutes}} left" }, + "screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetDescription": { + "message": "Your Audio Recording will be permanently deleted. This cannot be undone." + }, + "screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetPrimaryButtonText": { + "message": "Delete" + }, + "screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetSecondaryButtonText": { + "message": "Cancel" + }, + "screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetTitle": { + "message": "Delete?" + }, "screens.AudioScreen.CreateRecording.RecordingIdle.description": { "message": "Record up to {length} {length, plural, one {minute} other {minutes}}" }, + "screens.AudioScreen.Playback.description": { + "message": "Total length: {length}" + }, "screens.CameraScreen.goToSettings": { "message": "Go to Settings" }, From e12150871a7cfed151a43d203511e93478a44a25 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 26 Sep 2024 17:11:44 -0400 Subject: [PATCH 07/44] Adds play arrow image. --- src/frontend/images/PlayArrow.svg | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 src/frontend/images/PlayArrow.svg diff --git a/src/frontend/images/PlayArrow.svg b/src/frontend/images/PlayArrow.svg new file mode 100644 index 000000000..31800da4f --- /dev/null +++ b/src/frontend/images/PlayArrow.svg @@ -0,0 +1,3 @@ + + + \ No newline at end of file From 7583f81a8d7e805723913ede2f68d99b17ebb79f Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 30 Sep 2024 14:00:36 -0400 Subject: [PATCH 08/44] Playback working and can play from midway. --- .../screens/Audio/useAudioPlayback.ts | 102 +++++++++++------- 1 file changed, 63 insertions(+), 39 deletions(-) diff --git a/src/frontend/screens/Audio/useAudioPlayback.ts b/src/frontend/screens/Audio/useAudioPlayback.ts index deac3691c..38abb42c9 100644 --- a/src/frontend/screens/Audio/useAudioPlayback.ts +++ b/src/frontend/screens/Audio/useAudioPlayback.ts @@ -1,74 +1,98 @@ import {Audio, AVPlaybackStatus, AVPlaybackStatusSuccess} from 'expo-av'; -import {useCallback, useEffect, useState} from 'react'; +import {useCallback, useEffect, useState, useRef} from 'react'; import {Sound} from 'expo-av/build/Audio/Sound'; + export const useAudioPlayback = (recordingUri: string) => { - const [recordedSound, setRecordedSound] = useState(null); + const recordedSoundRef = useRef(null); const [isPlaying, setPlaying] = useState(false); const [duration, setDuration] = useState(0); const [currentPosition, setCurrentPosition] = useState(0); const [isReady, setReady] = useState(false); + const [hasFinished, setHasFinished] = useState(false); + const isPlayingRef = useRef(false); + const audioCallbackHandler = useCallback((status: AVPlaybackStatus) => { const update = status as AVPlaybackStatusSuccess; if (update.didJustFinish) { setPlaying(false); - setCurrentPosition(0); + setHasFinished(true); + setCurrentPosition(update.durationMillis ?? 0); } else { setPlaying(update.isPlaying); if (update.isPlaying) { setCurrentPosition(update.positionMillis); + setHasFinished(false); } } }, []); + useEffect(() => { - Audio.Sound.createAsync({ - uri: recordingUri, - }).then(({sound, status}) => { - if ('error' in status && status.error) { - console.error('error while creating audio playback', status.error); - return; - } - const successStatus = status as AVPlaybackStatusSuccess; - setRecordedSound(sound); - setDuration(successStatus.durationMillis!); - setReady(true); - sound.setOnPlaybackStatusUpdate(audioCallbackHandler); - }); + let soundInstance: Sound | null = null; + Audio.Sound.createAsync({uri: recordingUri}) + .then(({sound, status}) => { + if ('error' in status && status.error) { + console.error('Error while creating audio playback', status.error); + return; + } + soundInstance = sound; + recordedSoundRef.current = sound; + const successStatus = status as AVPlaybackStatusSuccess; + setDuration(successStatus.durationMillis ?? 0); + setReady(true); + sound.setOnPlaybackStatusUpdate(audioCallbackHandler); + }) + .catch(error => console.error('Error loading sound:', error)); + return () => { - if (recordedSound) { - recordedSound.unloadAsync(); + if (recordedSoundRef.current && !isPlayingRef.current) { + recordedSoundRef.current + .unloadAsync() + .catch(err => console.error('Unload error:', err)); } }; - }, [audioCallbackHandler, duration, recordingUri, recordedSound]); + }, [recordingUri]); + const startPlayback = async () => { - if (!isReady) { - console.warn( - 'startPlayback from useAudioPlayback called while recording is not ready', - ); + if (isPlayingRef.current || !isReady) { + console.warn('Playback is already in progress or not ready'); return; } - if (isPlaying) { - console.warn( - 'startPlayback from useAudioPlayback called while player is already in playing state', - ); - return; + isPlayingRef.current = true; + try { + const status = await recordedSoundRef.current!.getStatusAsync(); + if (hasFinished || status.positionMillis >= status.durationMillis) { + await recordedSoundRef.current!.setPositionAsync(0); + setCurrentPosition(0); + setHasFinished(false); + } + + await recordedSoundRef.current!.playAsync(); + const newStatus = await recordedSoundRef.current!.getStatusAsync(); + if (!newStatus.isLoaded) { + console.error('Playback failed - Sound is not loaded!'); + } + } catch (error) { + console.error('Failed to play sound:', error); + } finally { + isPlayingRef.current = false; } - await recordedSound!.replayAsync(); }; + const stopPlayback = async () => { - if (!isReady) { - console.warn( - 'stopPlayback from useAudioPlayback called while recording is not ready', - ); + if (isPlayingRef.current || !isReady || !isPlaying) { + console.warn('Playback is not in progress or not ready to stop'); return; } - if (!isPlaying) { - console.warn( - 'stopPlayback from useAudioPlayback called while player is not in playing state', - ); - return; + isPlayingRef.current = true; + try { + await recordedSoundRef.current!.pauseAsync(); + } catch (error) { + console.error('Failed to stop sound:', error); + } finally { + isPlayingRef.current = false; } - await recordedSound!.stopAsync(); }; + return { duration, isReady, From c37d204845da293b542dee51276b77f5c79b6206 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 30 Sep 2024 14:12:15 -0400 Subject: [PATCH 09/44] Tightens playback. --- .../screens/Audio/useAudioPlayback.ts | 43 ++++++------------- 1 file changed, 12 insertions(+), 31 deletions(-) diff --git a/src/frontend/screens/Audio/useAudioPlayback.ts b/src/frontend/screens/Audio/useAudioPlayback.ts index 38abb42c9..696fb94ea 100644 --- a/src/frontend/screens/Audio/useAudioPlayback.ts +++ b/src/frontend/screens/Audio/useAudioPlayback.ts @@ -8,20 +8,16 @@ export const useAudioPlayback = (recordingUri: string) => { const [duration, setDuration] = useState(0); const [currentPosition, setCurrentPosition] = useState(0); const [isReady, setReady] = useState(false); - const [hasFinished, setHasFinished] = useState(false); - const isPlayingRef = useRef(false); const audioCallbackHandler = useCallback((status: AVPlaybackStatus) => { const update = status as AVPlaybackStatusSuccess; if (update.didJustFinish) { setPlaying(false); - setHasFinished(true); setCurrentPosition(update.durationMillis ?? 0); } else { setPlaying(update.isPlaying); if (update.isPlaying) { setCurrentPosition(update.positionMillis); - setHasFinished(false); } } }, []); @@ -36,60 +32,45 @@ export const useAudioPlayback = (recordingUri: string) => { } soundInstance = sound; recordedSoundRef.current = sound; - const successStatus = status as AVPlaybackStatusSuccess; - setDuration(successStatus.durationMillis ?? 0); + setDuration((status as AVPlaybackStatusSuccess).durationMillis ?? 0); setReady(true); sound.setOnPlaybackStatusUpdate(audioCallbackHandler); }) .catch(error => console.error('Error loading sound:', error)); return () => { - if (recordedSoundRef.current && !isPlayingRef.current) { - recordedSoundRef.current + if (soundInstance) { + soundInstance .unloadAsync() .catch(err => console.error('Unload error:', err)); } }; - }, [recordingUri]); + }, [recordingUri, audioCallbackHandler]); const startPlayback = async () => { - if (isPlayingRef.current || !isReady) { - console.warn('Playback is already in progress or not ready'); - return; - } - isPlayingRef.current = true; + if (!isReady || isPlaying) return; + try { - const status = await recordedSoundRef.current!.getStatusAsync(); - if (hasFinished || status.positionMillis >= status.durationMillis) { + if (currentPosition >= duration) { await recordedSoundRef.current!.setPositionAsync(0); setCurrentPosition(0); - setHasFinished(false); } await recordedSoundRef.current!.playAsync(); - const newStatus = await recordedSoundRef.current!.getStatusAsync(); - if (!newStatus.isLoaded) { - console.error('Playback failed - Sound is not loaded!'); - } + setPlaying(true); } catch (error) { console.error('Failed to play sound:', error); - } finally { - isPlayingRef.current = false; } }; const stopPlayback = async () => { - if (isPlayingRef.current || !isReady || !isPlaying) { - console.warn('Playback is not in progress or not ready to stop'); - return; - } - isPlayingRef.current = true; + if (!isReady || !isPlaying) return; + try { await recordedSoundRef.current!.pauseAsync(); + setPlaying(false); } catch (error) { - console.error('Failed to stop sound:', error); - } finally { - isPlayingRef.current = false; + console.error('Failed to pause sound:', error); } }; From c0ccfaa23c9972c28198edbb21ea547e9aa10589 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Tue, 1 Oct 2024 15:20:31 -0400 Subject: [PATCH 10/44] Changes from Erik etc. --- src/frontend/lib/file-system.ts | 10 +- .../Audio/CreateRecording/RecordingDone.tsx | 4 +- .../Audio/CreateRecording/RecordingIdle.tsx | 17 +-- .../screens/Audio/CreateRecording/index.tsx | 66 ++++----- .../CreateRecording/useAudioRecording.ts | 128 ++++-------------- 5 files changed, 51 insertions(+), 174 deletions(-) diff --git a/src/frontend/lib/file-system.ts b/src/frontend/lib/file-system.ts index 7d6760634..c05faf278 100644 --- a/src/frontend/lib/file-system.ts +++ b/src/frontend/lib/file-system.ts @@ -1,15 +1,7 @@ -import { - ExternalDirectoryPath, - unlink as rnUnlink, -} from '@dr.pogodin/react-native-fs'; +import {ExternalDirectoryPath} from '@dr.pogodin/react-native-fs'; export function convertFileUriToPosixPath(fileUri: string) { return fileUri.replace(/^file:\/\//, ''); } -export async function unlink(fileUri: string): Promise { - const posixPath = convertFileUriToPosixPath(fileUri); - return rnUnlink(posixPath); -} - export {ExternalDirectoryPath as EXTERNAL_FILES_DIR}; diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 5828e9e30..4dcbb55e3 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -37,13 +37,11 @@ const m = defineMessages({ }); export function RecordingDone({ - createdAt, duration, uri, onDelete, onRecordAnother, }: { - createdAt: number; duration: number; uri: string; onDelete: () => void; @@ -78,7 +76,7 @@ export function RecordingDone({ {...props} onPress={() => { const audioRecording = { - createdAt, + createdAt: Date.now(), duration, uri, }; diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx index d41adadb6..55eed7d81 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx @@ -1,8 +1,5 @@ -import React, {useEffect} from 'react'; +import React from 'react'; import {defineMessages, useIntl} from 'react-intl'; - -import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; -import {CustomHeaderLeft} from '../../../sharedComponents/CustomHeaderLeft'; import {ContentWithControls} from '../ContentWithControls'; import * as Controls from '../Controls'; import {MAX_RECORDING_DURATION_MS} from '../constants'; @@ -16,20 +13,8 @@ const m = defineMessages({ }); export function RecordingIdle({onPressRecord}: {onPressRecord: () => void}) { - const navigation = useNavigationFromRoot(); const {formatMessage: t} = useIntl(); - useEffect(() => { - navigation.setOptions({ - headerLeft: props => ( - - ), - }); - }, [navigation]); - return ( { - const unsubscribe = navigation.addListener('focus', () => { - recordingState.reset().catch(error => { - console.error('Error resetting recording:', error); - }); - }); - - return unsubscribe; - }, [navigation, recordingState]); + if (!status || (!status.isRecording && !uri)) { + return ; + } - switch (recordingState.status) { - case 'idle': { - return ; - } - case 'active': { - return ( - - ); - } - case 'done': { - return ( - { - await recordingState.deleteRecording().catch(err => { - console.log(err); - }); - navigation.goBack(); - }} - onRecordAnother={async () => { - await recordingState.reset(); - }} - /> - ); - } + if (status.isRecording) { + return ( + + ); } + + return ( + { + reset(); + navigation.goBack(); + }} + onRecordAnother={reset} + /> + ); } diff --git a/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts index a383fe4cf..5e1c57ee2 100644 --- a/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts +++ b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts @@ -1,117 +1,35 @@ import {useState} from 'react'; import {Audio} from 'expo-av'; -import {unlink} from '../../../lib/file-system'; -type AudioRecordingIdle = { - status: 'idle'; - startRecording: () => Promise; - reset: () => Promise; -}; - -type AudioRecordingActive = { - status: 'active'; - /** - * Time elapsed in milliseconds - */ - duration: number; - uri: string; - createdAt: number; - stopRecording: () => Promise; - reset: () => Promise; -}; - -type AudioRecordingDone = { - status: 'done'; - /** - * Time elapsed in milliseconds - */ - duration: number; - uri: string; - createdAt: number; - deleteRecording: () => Promise; - reset: () => Promise; -}; - -type AudioRecordingState = - | AudioRecordingIdle - | AudioRecordingActive - | AudioRecordingDone; - -export function useAudioRecording(): AudioRecordingState { - const [state, setState] = useState<{ - createdAt: number; - recording: Audio.Recording; - status: Audio.RecordingStatus; - uri: string; - } | null>(null); +export function useAudioRecording() { + const [recording, setRecording] = useState(null); + const [status, setStatus] = useState(null); + const [uri, setUri] = useState(''); + + async function startRecording() { + const {recording: audioRecording} = await Audio.Recording.createAsync( + Audio.RecordingOptionsPresets.HIGH_QUALITY, + stat => setStatus(stat), + ); + setRecording(audioRecording); + } const reset = async () => { - if (state) { - if (state.status.isRecording) { - await state.recording.stopAndUnloadAsync(); + if (recording) { + if ((await recording.getStatusAsync()).isRecording) { + await recording.stopAndUnloadAsync(); } } - setState(null); + setRecording(null); + setStatus(null); + setUri(null); }; - if (!state) { - return { - status: 'idle', - startRecording: async () => { - const createdAt = Date.now(); - const {recording, status} = await Audio.Recording.createAsync( - Audio.RecordingOptionsPresets.HIGH_QUALITY, - status => { - setState(prev => { - if (!prev) return prev; - return { - ...prev, - status, - }; - }); - }, - 1000, - ); - - const uri = recording.getURI(); - - // Should not happen - if (uri === null) { - throw new Error('Could not get URI for recording'); - } - - setState({createdAt, recording, status, uri}); - }, - reset, - }; - } - - if (state.status.isDoneRecording) { - return { - status: 'done', - duration: state.status.durationMillis, - uri: state.uri, - createdAt: state.createdAt, - deleteRecording: async () => { - return unlink(state.uri); - }, - reset, - }; + async function stopRecording() { + if (!recording) return; + await recording.stopAndUnloadAsync(); + setUri(recording.getURI()); } - return { - status: 'active', - duration: state.status.durationMillis, - uri: state.uri, - createdAt: state.createdAt, - stopRecording: async () => { - const status = await state.recording.stopAndUnloadAsync(); - - setState(prev => { - if (!prev) return prev; - return {...prev, status}; - }); - }, - reset, - }; + return {reset, startRecording, stopRecording, status, uri}; } From 2b4be6d6dca04d820501854931b751b46416ec53 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Tue, 1 Oct 2024 15:28:09 -0400 Subject: [PATCH 11/44] Adds back header for when returning to record another. --- .../Audio/CreateRecording/RecordingIdle.tsx | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx index 55eed7d81..428801d00 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx @@ -1,5 +1,7 @@ -import React from 'react'; +import React, {useEffect} from 'react'; import {defineMessages, useIntl} from 'react-intl'; +import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; +import {CustomHeaderLeft} from '../../../sharedComponents/CustomHeaderLeft'; import {ContentWithControls} from '../ContentWithControls'; import * as Controls from '../Controls'; import {MAX_RECORDING_DURATION_MS} from '../constants'; @@ -13,8 +15,20 @@ const m = defineMessages({ }); export function RecordingIdle({onPressRecord}: {onPressRecord: () => void}) { + const navigation = useNavigationFromRoot(); const {formatMessage: t} = useIntl(); + useEffect(() => { + navigation.setOptions({ + headerLeft: props => ( + + ), + }); + }, [navigation]); + return ( Date: Wed, 2 Oct 2024 12:20:35 -0400 Subject: [PATCH 12/44] Wraps everything in row. Prevents going back. Removes header.Adds two modals to the RecordingDone component. --- .../screens/Audio/ContentWithControls.tsx | 3 +- .../Audio/CreateRecording/RecordingActive.tsx | 8 +- .../Audio/CreateRecording/RecordingDone.tsx | 184 +++++++++++++----- .../Audio/CreateRecording/RecordingIdle.tsx | 6 +- .../screens/Audio/CreateRecording/index.tsx | 55 ++++-- src/frontend/screens/Audio/Playback.tsx | 24 +-- .../screens/Audio/RecordingSuccessModal.tsx | 113 ----------- 7 files changed, 193 insertions(+), 200 deletions(-) delete mode 100644 src/frontend/screens/Audio/RecordingSuccessModal.tsx diff --git a/src/frontend/screens/Audio/ContentWithControls.tsx b/src/frontend/screens/Audio/ContentWithControls.tsx index 784745090..f16d8bbb7 100644 --- a/src/frontend/screens/Audio/ContentWithControls.tsx +++ b/src/frontend/screens/Audio/ContentWithControls.tsx @@ -6,6 +6,7 @@ import {Duration} from 'luxon'; import {MEDIUM_GREY, WHITE} from '../../lib/styles'; import {ScreenContentWithDock} from '../../sharedComponents/ScreenContentWithDock'; import {Text} from '../../sharedComponents/Text'; +import {Row as ControlsRow} from './Controls'; export function ContentWithControls({ controls, @@ -22,7 +23,7 @@ export function ContentWithControls({ + dockContent={{controls}}> diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx index 9d3d22351..810c6e725 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx @@ -36,7 +36,7 @@ export function RecordingActive({ }, [duration]); useEffect(() => { - navigation.setOptions({headerLeft: () => null}); + navigation.setOptions({headerShown: false}); }, [navigation]); useAutoStopRecording(minutesRemaining, onPressStop); @@ -52,11 +52,7 @@ export function RecordingActive({ }) : undefined } - controls={ - - - - } + controls={} /> diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 4dcbb55e3..3abb936fa 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -1,7 +1,7 @@ -import React, {useEffect} from 'react'; +import React, {useEffect, useState} from 'react'; import {HeaderBackButton} from '@react-navigation/elements'; -import {defineMessages, useIntl} from 'react-intl'; -import {Pressable} from 'react-native'; +import {defineMessages, useIntl, FormattedMessage} from 'react-intl'; +import {Pressable, Text, View, StyleSheet} from 'react-native'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; import {WHITE} from '../../../lib/styles'; @@ -12,9 +12,10 @@ import { } from '../../../sharedComponents/BottomSheetModal'; import {CloseIcon, DeleteIcon} from '../../../sharedComponents/icons'; import ErrorIcon from '../../../images/Error.svg'; +import SuccessIcon from '../../../images/GreenCheck.svg'; import {Playback} from '../Playback'; -import {RecordingSuccessModal} from '../RecordingSuccessModal'; import {useDraftObservation} from '../../../hooks/useDraftObservation'; +import {Button} from '../../../sharedComponents/Button'; const m = defineMessages({ deleteBottomSheetTitle: { @@ -34,36 +35,55 @@ const m = defineMessages({ id: 'screens.AudioScreen.CreateRecording.RecordingDone.deleteBottomSheetSecondaryButtonText', defaultMessage: 'Cancel', }, + successTitle: { + id: 'AudioPlaybackScreen.DeleteAudioRecordingModal.successTitle', + defaultMessage: 'Success!', + }, + successDescription: { + id: 'AudioPlaybackScreen.DeleteAudioRecordingModal.successDescription', + defaultMessage: 'Your {audioRecording} was added.', + }, + returnToEditorButtonText: { + id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.returnToEditor', + defaultMessage: 'Return to Editor', + }, + recordAnotherButtonText: { + id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.recordAnother', + defaultMessage: 'Record Another', + }, + audioRecording: { + id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.audioRecording', + defaultMessage: 'Audio Recording', + }, }); +interface RecordingDoneProps { + duration: number; + uri: string; + onDelete: () => void; + onRecordAnother: () => void; +} + +type ModalContentType = 'delete' | 'success' | null; + export function RecordingDone({ duration, uri, onDelete, onRecordAnother, -}: { - duration: number; - uri: string; - onDelete: () => void; - onRecordAnother: () => void; -}) { +}: RecordingDoneProps) { const {formatMessage: t} = useIntl(); const navigation = useNavigationFromRoot(); const {addAudio} = useDraftObservation(); - const { - sheetRef: deleteSheetRef, - isOpen: isDeleteSheetOpen, - openSheet: openDeleteSheet, - closeSheet: closeDeleteSheet, - } = useBottomSheetModal({ - openOnMount: false, - }); + + const [modalContentType, setModalContentType] = + useState(null); const { - sheetRef: successSheetRef, - isOpen: isSuccessSheetOpen, - openSheet: openSuccessSheet, - closeSheet: closeSuccessSheet, + sheetRef, + isOpen: isModalOpen, + openSheet, + closeSheet, } = useBottomSheetModal({ openOnMount: false, }); @@ -81,36 +101,29 @@ export function RecordingDone({ uri, }; addAudio(audioRecording); - openSuccessSheet(); + setModalContentType('success'); + openSheet(); }} backImage={props => } /> ), }); - }, [navigation, openSuccessSheet]); + }, [navigation, addAudio, duration, uri, openSheet]); const handleReturnToEditor = () => { - closeSuccessSheet(); + closeSheet(); navigation.navigate('ObservationCreate'); }; const handleRecordAnother = async () => { - closeSuccessSheet(); + closeSheet(); await onRecordAnother(); navigation.navigate('Audio'); }; - return ( - <> - - - - } - /> - + const renderModalContent = () => { + if (modalContentType === 'delete') { + return ( } title={t(m.deleteBottomSheetTitle)} @@ -121,7 +134,7 @@ export function RecordingDone({ text: t(m.deleteBottomSheetPrimaryButtonText), icon: , onPress: () => { - closeDeleteSheet(); + closeSheet(); onDelete(); }, variation: 'filled', @@ -130,18 +143,99 @@ export function RecordingDone({ variation: 'outlined', text: t(m.deleteBottomSheetSecondaryButtonText), onPress: () => { - closeDeleteSheet(); + closeSheet(); }, }, ]} /> - - + + + {t(m.successTitle)} + + ( + {message} + ), + }} + /> + + + + + + + + ); + } + return null; + }; + + return ( + <> + { + setModalContentType('delete'); + openSheet(); + }}> + + + } /> + + {renderModalContent()} + ); } + +const styles = StyleSheet.create({ + container: { + justifyContent: 'center', + alignItems: 'center', + height: '100%', + padding: 20, + paddingTop: 80, + }, + wrapper: { + alignItems: 'center', + flex: 1, + justifyContent: 'center', + }, + title: { + fontWeight: 'bold', + fontSize: 24, + marginTop: 10, + }, + description: {fontSize: 16, marginTop: 40}, + textBold: { + fontWeight: 'bold', + fontSize: 16, + }, + buttonContainer: { + width: '100%', + justifyContent: 'flex-end', + flex: 1, + gap: 15, + }, +}); diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx index 428801d00..150a110ad 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx @@ -35,11 +35,7 @@ export function RecordingIdle({onPressRecord}: {onPressRecord: () => void}) { length: MAX_RECORDING_DURATION_MS / 60_000, })} timeElapsed={0} - controls={ - - - - } + controls={} /> ); } diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 4f60856ee..c7f258d2a 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -1,4 +1,5 @@ -import React from 'react'; +import React, {useEffect} from 'react'; +import {BackHandler} from 'react-native'; import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; import {RecordingActive} from './RecordingActive'; import {RecordingDone} from './RecordingDone'; @@ -10,11 +11,33 @@ export function CreateRecording() { const {startRecording, stopRecording, reset, status, uri} = useAudioRecording(); - if (!status || (!status.isRecording && !uri)) { + let currentState = 'idle'; + if (status && status.isRecording) { + currentState = 'active'; + } else if (uri) { + currentState = 'done'; + } + + useEffect(() => { + if (currentState === 'active' || currentState === 'done') { + const onBackPress = () => { + return true; + }; + const backHandler = BackHandler.addEventListener( + 'hardwareBackPress', + onBackPress, + ); + return () => { + backHandler.remove(); + }; + } + }, [currentState]); + + if (currentState === 'idle') { return ; } - if (status.isRecording) { + if (currentState === 'active') { return ( { - reset(); - navigation.goBack(); - }} - onRecordAnother={reset} - /> - ); + if (currentState === 'done') { + return ( + { + navigation.goBack(); + reset(); + }} + onRecordAnother={reset} + /> + ); + } + + return null; } diff --git a/src/frontend/screens/Audio/Playback.tsx b/src/frontend/screens/Audio/Playback.tsx index 8b27bdf5f..955f6dc87 100644 --- a/src/frontend/screens/Audio/Playback.tsx +++ b/src/frontend/screens/Audio/Playback.tsx @@ -30,27 +30,19 @@ export function Playback({ })} progress={currentPosition / duration} controls={ - - {leftControl ? ( + <> + {leftControl && ( {leftControl} - ) : null} + )} {isPlaying ? ( - { - stopPlayback(); - }} - /> + ) : ( - { - startPlayback(); - }} - /> + )} - {rightControl ? ( + {rightControl && ( {rightControl} - ) : null} - + )} + } /> ); diff --git a/src/frontend/screens/Audio/RecordingSuccessModal.tsx b/src/frontend/screens/Audio/RecordingSuccessModal.tsx deleted file mode 100644 index 196502f50..000000000 --- a/src/frontend/screens/Audio/RecordingSuccessModal.tsx +++ /dev/null @@ -1,113 +0,0 @@ -import {BottomSheetModalMethods} from '@gorhom/bottom-sheet/lib/typescript/types'; -import React, {FC} from 'react'; -import {BottomSheetModal} from '../../sharedComponents/BottomSheetModal'; -import {defineMessages, FormattedMessage, useIntl} from 'react-intl'; -import {StyleSheet, Text, View} from 'react-native'; -import SuccessIcon from '../../images/GreenCheck.svg'; -import {Button} from '../../sharedComponents/Button'; - -const m = defineMessages({ - successTitle: { - id: 'AudioPlaybackScreen.DeleteAudioRecordingModal.successTitle', - defaultMessage: 'Success!', - }, - successDescription: { - id: 'AudioPlaybackScreen.DeleteAudioRecordingModal.successDescription', - defaultMessage: 'Your {audioRecording} was added.', - }, - returnToEditorButtonText: { - id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.returnToEditor', - defaultMessage: 'Return to Editor', - }, - recordAnotherButtonText: { - id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.recordAnother', - defaultMessage: 'Record Another', - }, - audioRecording: { - id: 'AudioPlaybackScreen.SuccessAudioRecordingModal.audioRecording', - defaultMessage: 'Audio Recording', - }, -}); - -interface RecordingSuccessModal { - sheetRef: React.RefObject; - isOpen: boolean; - onReturnToEditor: () => void; - onRecordAnother: () => void; -} - -export const RecordingSuccessModal: FC = ({ - sheetRef, - isOpen, - onReturnToEditor, - onRecordAnother, -}) => { - const {formatMessage} = useIntl(); - - return ( - - - - - {formatMessage(m.successTitle)} - - {message}, - }} - /> - - - - - - - - - ); -}; - -const styles = StyleSheet.create({ - container: { - justifyContent: 'center', - alignItems: 'center', - height: '100%', - padding: 20, - paddingTop: 80, - }, - wrapper: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - title: { - fontWeight: 'bold', - fontSize: 24, - marginTop: 10, - }, - description: {fontSize: 16, marginTop: 40}, - textBold: { - fontWeight: 'bold', - fontSize: 16, - }, -}); From d6c57c61d9cdfb1d400ef3c9b2b980ffc353ce50 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Wed, 2 Oct 2024 13:06:25 -0400 Subject: [PATCH 13/44] Use callback added. Changes to touchable opacity. Adjusts constant location. Adjusts playback location. Removes isready in state. --- src/frontend/hooks/useDraftObservation.ts | 10 +-------- .../screens/Audio/AnimatedBackground.tsx | 2 +- src/frontend/screens/Audio/Controls.tsx | 22 +++++++------------ .../Audio/CreateRecording/RecordingActive.tsx | 2 +- .../Audio/CreateRecording/RecordingDone.tsx | 8 +++---- .../Audio/CreateRecording/RecordingIdle.tsx | 2 +- .../CreateRecording/useAudioRecording.ts | 16 +++++++------- .../CreateRecording/useAutoStopRecording.ts | 2 +- src/frontend/screens/Audio/constants.ts | 1 - src/frontend/screens/Audio/index.tsx | 2 ++ .../screens/Audio/useAudioPlayback.ts | 7 ++---- .../screens/ObservationCreate/index.tsx | 1 + .../Audio => sharedComponents}/Playback.tsx | 6 ++--- 13 files changed, 33 insertions(+), 48 deletions(-) delete mode 100644 src/frontend/screens/Audio/constants.ts rename src/frontend/{screens/Audio => sharedComponents}/Playback.tsx (86%) diff --git a/src/frontend/hooks/useDraftObservation.ts b/src/frontend/hooks/useDraftObservation.ts index f19765d68..c45a3c648 100644 --- a/src/frontend/hooks/useDraftObservation.ts +++ b/src/frontend/hooks/useDraftObservation.ts @@ -8,7 +8,6 @@ import { PhotoPromiseWithMetadata, UnprocessedDraftPhoto, } from '../contexts/PhotoPromiseContext/types'; -import {AudioRecording} from '../sharedTypes'; // react native does not have a random bytes generator, `non-secure` does not require a random bytes generator. import {nanoid} from 'nanoid/non-secure'; import * as Sentry from '@sentry/react-native'; @@ -94,13 +93,6 @@ export const useDraftObservation = () => { [deletePersistedPhoto, deletePhotoPromise], ); - const addAudio = useCallback( - (audioRecording: AudioRecording) => { - addAudioRecording(audioRecording); - }, - [addAudioRecording], - ); - return { addPhoto, clearDraft, @@ -111,6 +103,6 @@ export const useDraftObservation = () => { updatePreset, usePreset, existingObservationToDraft, - addAudio, + addAudioRecording, }; }; diff --git a/src/frontend/screens/Audio/AnimatedBackground.tsx b/src/frontend/screens/Audio/AnimatedBackground.tsx index 07a60c10d..4ca6630cd 100644 --- a/src/frontend/screens/Audio/AnimatedBackground.tsx +++ b/src/frontend/screens/Audio/AnimatedBackground.tsx @@ -3,7 +3,7 @@ import {Dimensions, StyleSheet} from 'react-native'; import Animated, {SharedValue, useAnimatedStyle} from 'react-native-reanimated'; import {useSafeAreaInsets} from 'react-native-safe-area-context'; -import {MAX_RECORDING_DURATION_MS} from './constants'; +import {MAX_RECORDING_DURATION_MS} from './index'; export function AnimatedBackground({ elapsedTimeValue, diff --git a/src/frontend/screens/Audio/Controls.tsx b/src/frontend/screens/Audio/Controls.tsx index 4cb63471c..1090b2c1c 100644 --- a/src/frontend/screens/Audio/Controls.tsx +++ b/src/frontend/screens/Audio/Controls.tsx @@ -1,8 +1,8 @@ import React, {PropsWithChildren} from 'react'; import { Dimensions, - Pressable, - PressableProps, + TouchableOpacity, + TouchableOpacityProps, StyleSheet, View, } from 'react-native'; @@ -10,21 +10,15 @@ import { import PlayArrow from '../../images/PlayArrow.svg'; import {MAGENTA, BLACK, LIGHT_GREY, WHITE} from '../../lib/styles'; -type BaseProps = PropsWithChildren; +type BaseProps = PropsWithChildren; -function ControlButtonPrimaryBase({children, ...pressableProps}: BaseProps) { +function ControlButtonPrimaryBase({children, ...touchableProps}: BaseProps) { return ( - [ - styles.basePressable, - typeof pressableProps.style === 'function' - ? pressableProps.style({pressed}) - : pressableProps.style, - pressed && styles.pressablePressed, - ]}> + {children} - + ); } diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx index 810c6e725..4505f8f44 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingActive.tsx @@ -6,7 +6,7 @@ import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; import {AnimatedBackground} from '../AnimatedBackground'; import {ContentWithControls} from '../ContentWithControls'; import * as Controls from '../Controls'; -import {MAX_RECORDING_DURATION_MS} from '../constants'; +import {MAX_RECORDING_DURATION_MS} from '../index'; import {useAutoStopRecording} from './useAutoStopRecording'; const m = defineMessages({ diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 3abb936fa..362d5faf3 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -13,7 +13,7 @@ import { import {CloseIcon, DeleteIcon} from '../../../sharedComponents/icons'; import ErrorIcon from '../../../images/Error.svg'; import SuccessIcon from '../../../images/GreenCheck.svg'; -import {Playback} from '../Playback'; +import {Playback} from '../../../sharedComponents/Playback'; import {useDraftObservation} from '../../../hooks/useDraftObservation'; import {Button} from '../../../sharedComponents/Button'; @@ -74,7 +74,7 @@ export function RecordingDone({ }: RecordingDoneProps) { const {formatMessage: t} = useIntl(); const navigation = useNavigationFromRoot(); - const {addAudio} = useDraftObservation(); + const {addAudioRecording} = useDraftObservation(); const [modalContentType, setModalContentType] = useState(null); @@ -100,7 +100,7 @@ export function RecordingDone({ duration, uri, }; - addAudio(audioRecording); + addAudioRecording(audioRecording); setModalContentType('success'); openSheet(); }} @@ -108,7 +108,7 @@ export function RecordingDone({ /> ), }); - }, [navigation, addAudio, duration, uri, openSheet]); + }, [navigation, addAudioRecording, duration, uri, openSheet]); const handleReturnToEditor = () => { closeSheet(); diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx index 150a110ad..f19eca4aa 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingIdle.tsx @@ -4,7 +4,7 @@ import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; import {CustomHeaderLeft} from '../../../sharedComponents/CustomHeaderLeft'; import {ContentWithControls} from '../ContentWithControls'; import * as Controls from '../Controls'; -import {MAX_RECORDING_DURATION_MS} from '../constants'; +import {MAX_RECORDING_DURATION_MS} from '../index'; const m = defineMessages({ description: { diff --git a/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts index 5e1c57ee2..88279e531 100644 --- a/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts +++ b/src/frontend/screens/Audio/CreateRecording/useAudioRecording.ts @@ -1,20 +1,20 @@ -import {useState} from 'react'; +import {useState, useCallback} from 'react'; import {Audio} from 'expo-av'; export function useAudioRecording() { const [recording, setRecording] = useState(null); const [status, setStatus] = useState(null); - const [uri, setUri] = useState(''); + const [uri, setUri] = useState(null); - async function startRecording() { + const startRecording = useCallback(async () => { const {recording: audioRecording} = await Audio.Recording.createAsync( Audio.RecordingOptionsPresets.HIGH_QUALITY, stat => setStatus(stat), ); setRecording(audioRecording); - } + }, [setRecording, setStatus]); - const reset = async () => { + const reset = useCallback(async () => { if (recording) { if ((await recording.getStatusAsync()).isRecording) { await recording.stopAndUnloadAsync(); @@ -23,13 +23,13 @@ export function useAudioRecording() { setRecording(null); setStatus(null); setUri(null); - }; + }, [recording, setRecording, setStatus, setUri]); - async function stopRecording() { + const stopRecording = useCallback(async () => { if (!recording) return; await recording.stopAndUnloadAsync(); setUri(recording.getURI()); - } + }, [recording, setUri]); return {reset, startRecording, stopRecording, status, uri}; } diff --git a/src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts b/src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts index cdcf55d40..3372dfef1 100644 --- a/src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts +++ b/src/frontend/screens/Audio/CreateRecording/useAutoStopRecording.ts @@ -5,7 +5,7 @@ export function useAutoStopRecording( onPressStop: () => void, ) { useEffect(() => { - if (minutesRemaining === 0) { + if (minutesRemaining <= 0) { onPressStop(); } }, [minutesRemaining, onPressStop]); diff --git a/src/frontend/screens/Audio/constants.ts b/src/frontend/screens/Audio/constants.ts deleted file mode 100644 index 6462d61b0..000000000 --- a/src/frontend/screens/Audio/constants.ts +++ /dev/null @@ -1 +0,0 @@ -export const MAX_RECORDING_DURATION_MS = 5 * 60_000; diff --git a/src/frontend/screens/Audio/index.tsx b/src/frontend/screens/Audio/index.tsx index 8549f37ed..ec1b82eef 100644 --- a/src/frontend/screens/Audio/index.tsx +++ b/src/frontend/screens/Audio/index.tsx @@ -6,6 +6,8 @@ import {CustomHeaderLeft} from '../../sharedComponents/CustomHeaderLeft'; import {StatusBar} from 'expo-status-bar'; import {CreateRecording} from './CreateRecording'; +export const MAX_RECORDING_DURATION_MS = 5 * 60_000; + export function Audio() { return ( <> diff --git a/src/frontend/screens/Audio/useAudioPlayback.ts b/src/frontend/screens/Audio/useAudioPlayback.ts index 696fb94ea..d948602d3 100644 --- a/src/frontend/screens/Audio/useAudioPlayback.ts +++ b/src/frontend/screens/Audio/useAudioPlayback.ts @@ -7,7 +7,6 @@ export const useAudioPlayback = (recordingUri: string) => { const [isPlaying, setPlaying] = useState(false); const [duration, setDuration] = useState(0); const [currentPosition, setCurrentPosition] = useState(0); - const [isReady, setReady] = useState(false); const audioCallbackHandler = useCallback((status: AVPlaybackStatus) => { const update = status as AVPlaybackStatusSuccess; @@ -33,7 +32,6 @@ export const useAudioPlayback = (recordingUri: string) => { soundInstance = sound; recordedSoundRef.current = sound; setDuration((status as AVPlaybackStatusSuccess).durationMillis ?? 0); - setReady(true); sound.setOnPlaybackStatusUpdate(audioCallbackHandler); }) .catch(error => console.error('Error loading sound:', error)); @@ -48,7 +46,7 @@ export const useAudioPlayback = (recordingUri: string) => { }, [recordingUri, audioCallbackHandler]); const startPlayback = async () => { - if (!isReady || isPlaying) return; + if (!recordedSoundRef.current || isPlaying) return; try { if (currentPosition >= duration) { @@ -64,7 +62,7 @@ export const useAudioPlayback = (recordingUri: string) => { }; const stopPlayback = async () => { - if (!isReady || !isPlaying) return; + if (!recordedSoundRef.current || !isPlaying) return; try { await recordedSoundRef.current!.pauseAsync(); @@ -76,7 +74,6 @@ export const useAudioPlayback = (recordingUri: string) => { return { duration, - isReady, isPlaying, currentPosition, startPlayback, diff --git a/src/frontend/screens/ObservationCreate/index.tsx b/src/frontend/screens/ObservationCreate/index.tsx index fce840fbb..b0d539630 100644 --- a/src/frontend/screens/ObservationCreate/index.tsx +++ b/src/frontend/screens/ObservationCreate/index.tsx @@ -298,6 +298,7 @@ export const ObservationCreate = ({ navigation, createBlobMutation.isPending, createObservationMutation.isPending, + createAudioBlobMutation.isPending, checkAccuracyAndLocation, ]); diff --git a/src/frontend/screens/Audio/Playback.tsx b/src/frontend/sharedComponents/Playback.tsx similarity index 86% rename from src/frontend/screens/Audio/Playback.tsx rename to src/frontend/sharedComponents/Playback.tsx index 955f6dc87..ad2b127e0 100644 --- a/src/frontend/screens/Audio/Playback.tsx +++ b/src/frontend/sharedComponents/Playback.tsx @@ -1,9 +1,9 @@ import React, {ReactNode} from 'react'; import {Duration} from 'luxon'; import {defineMessages, useIntl} from 'react-intl'; -import {ContentWithControls} from './ContentWithControls'; -import * as Controls from './Controls'; -import {useAudioPlayback} from './useAudioPlayback'; +import {ContentWithControls} from '../screens/Audio/ContentWithControls'; +import * as Controls from '../screens/Audio/Controls'; +import {useAudioPlayback} from '../screens/Audio/useAudioPlayback'; const m = defineMessages({ description: { id: 'screens.AudioScreen.Playback.description', From 08d5196b9693f96de43efc3a51cc7f858c58a4a3 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Wed, 2 Oct 2024 16:08:41 -0400 Subject: [PATCH 14/44] Uses use callback. Changes if to ternary. Uses BottomModalContent. --- .../Audio/CreateRecording/RecordingDone.tsx | 95 +++++++------------ .../screens/Audio/CreateRecording/index.tsx | 11 +-- .../screens/Audio/useAudioPlayback.ts | 8 +- 3 files changed, 40 insertions(+), 74 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 362d5faf3..340cedb91 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -1,7 +1,7 @@ import React, {useEffect, useState} from 'react'; import {HeaderBackButton} from '@react-navigation/elements'; import {defineMessages, useIntl, FormattedMessage} from 'react-intl'; -import {Pressable, Text, View, StyleSheet} from 'react-native'; +import {Pressable, Text, View} from 'react-native'; import MaterialIcon from 'react-native-vector-icons/MaterialIcons'; import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; import {WHITE} from '../../../lib/styles'; @@ -15,7 +15,6 @@ import ErrorIcon from '../../../images/Error.svg'; import SuccessIcon from '../../../images/GreenCheck.svg'; import {Playback} from '../../../sharedComponents/Playback'; import {useDraftObservation} from '../../../hooks/useDraftObservation'; -import {Button} from '../../../sharedComponents/Button'; const m = defineMessages({ deleteBottomSheetTitle: { @@ -150,35 +149,40 @@ export function RecordingDone({ /> ); } else if (modalContentType === 'success') { + const description = ( + + + ( + {message} + ), + }} + /> + + + ); return ( - - - - {t(m.successTitle)} - - ( - {message} - ), - }} - /> - - - - - - + + } + title={t(m.successTitle)} + description={description} + buttonConfigs={[ + { + text: t(m.returnToEditorButtonText), + onPress: handleReturnToEditor, + variation: 'outlined', + }, + { + text: t(m.recordAnotherButtonText), + onPress: handleRecordAnother, + variation: 'filled', + }, + ]} + /> ); } @@ -208,34 +212,3 @@ export function RecordingDone({ ); } - -const styles = StyleSheet.create({ - container: { - justifyContent: 'center', - alignItems: 'center', - height: '100%', - padding: 20, - paddingTop: 80, - }, - wrapper: { - alignItems: 'center', - flex: 1, - justifyContent: 'center', - }, - title: { - fontWeight: 'bold', - fontSize: 24, - marginTop: 10, - }, - description: {fontSize: 16, marginTop: 40}, - textBold: { - fontWeight: 'bold', - fontSize: 16, - }, - buttonContainer: { - width: '100%', - justifyContent: 'flex-end', - flex: 1, - gap: 15, - }, -}); diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index c7f258d2a..6e2c76a49 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -11,12 +11,7 @@ export function CreateRecording() { const {startRecording, stopRecording, reset, status, uri} = useAudioRecording(); - let currentState = 'idle'; - if (status && status.isRecording) { - currentState = 'active'; - } else if (uri) { - currentState = 'done'; - } + const currentState = status?.isRecording ? 'active' : uri ? 'done' : 'idle'; useEffect(() => { if (currentState === 'active' || currentState === 'done') { @@ -52,13 +47,11 @@ export function CreateRecording() { uri={uri || ''} duration={status?.durationMillis || 0} onDelete={() => { - navigation.goBack(); reset(); + navigation.goBack(); }} onRecordAnother={reset} /> ); } - - return null; } diff --git a/src/frontend/screens/Audio/useAudioPlayback.ts b/src/frontend/screens/Audio/useAudioPlayback.ts index d948602d3..7a133980c 100644 --- a/src/frontend/screens/Audio/useAudioPlayback.ts +++ b/src/frontend/screens/Audio/useAudioPlayback.ts @@ -45,7 +45,7 @@ export const useAudioPlayback = (recordingUri: string) => { }; }, [recordingUri, audioCallbackHandler]); - const startPlayback = async () => { + const startPlayback = useCallback(async () => { if (!recordedSoundRef.current || isPlaying) return; try { @@ -59,9 +59,9 @@ export const useAudioPlayback = (recordingUri: string) => { } catch (error) { console.error('Failed to play sound:', error); } - }; + }, [isPlaying, currentPosition, duration]); - const stopPlayback = async () => { + const stopPlayback = useCallback(async () => { if (!recordedSoundRef.current || !isPlaying) return; try { @@ -70,7 +70,7 @@ export const useAudioPlayback = (recordingUri: string) => { } catch (error) { console.error('Failed to pause sound:', error); } - }; + }, [isPlaying]); return { duration, From 19dc94a32705b46a228df26ebcee4d649c71c138 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Wed, 2 Oct 2024 18:02:58 -0400 Subject: [PATCH 15/44] Trying to handle dismissing modal when app is closed and restarted. --- .../screens/Audio/CreateRecording/RecordingDone.tsx | 6 ++++++ src/frontend/screens/Audio/CreateRecording/index.tsx | 6 +++++- 2 files changed, 11 insertions(+), 1 deletion(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 340cedb91..5a2b155f9 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -15,6 +15,7 @@ import ErrorIcon from '../../../images/Error.svg'; import SuccessIcon from '../../../images/GreenCheck.svg'; import {Playback} from '../../../sharedComponents/Playback'; import {useDraftObservation} from '../../../hooks/useDraftObservation'; +import {useBottomSheetModal as useGorhamBottomSheet} from '@gorhom/bottom-sheet'; const m = defineMessages({ deleteBottomSheetTitle: { @@ -87,6 +88,8 @@ export function RecordingDone({ openOnMount: false, }); + const {dismissAll} = useGorhamBottomSheet(); + useEffect(() => { navigation.setOptions({ headerShown: true, @@ -134,6 +137,9 @@ export function RecordingDone({ icon: , onPress: () => { closeSheet(); + if (!sheetRef.current) { + dismissAll(); + } onDelete(); }, variation: 'filled', diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 6e2c76a49..c899f0635 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -48,7 +48,11 @@ export function CreateRecording() { duration={status?.durationMillis || 0} onDelete={() => { reset(); - navigation.goBack(); + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.navigate('ObservationCreate'); + } }} onRecordAnother={reset} /> From a4ff0d0e0c90f5605f4744214036e42435c53817 Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:14:02 -0700 Subject: [PATCH 16/44] chore: update icon on error bottom sheet --- src/frontend/sharedComponents/ErrorBottomSheet.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/frontend/sharedComponents/ErrorBottomSheet.tsx b/src/frontend/sharedComponents/ErrorBottomSheet.tsx index ef3fa23f0..14a2f3429 100644 --- a/src/frontend/sharedComponents/ErrorBottomSheet.tsx +++ b/src/frontend/sharedComponents/ErrorBottomSheet.tsx @@ -8,7 +8,7 @@ import { BottomSheetModal, useBottomSheetModal, } from './BottomSheetModal'; -import {LogoWithErrorIcon} from './LogoWithErrorIcon'; +import ErrorIcon from '../images/Error.svg'; const m = defineMessages({ somethingWrong: { @@ -74,7 +74,7 @@ export const ErrorBottomSheet = (props: ErrorModalProps) => { onDismiss={closeSheet} isOpen={isOpen}> } + icon={} title={formatMessage(m.somethingWrong)} buttonConfigs={buttonConfigs} /> From 3093210dcc106d7bb50b9bd976bb63e93d782f3a Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Wed, 2 Oct 2024 16:33:02 -0700 Subject: [PATCH 17/44] chore: added ability for custom message --- src/frontend/sharedComponents/ErrorBottomSheet.tsx | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/frontend/sharedComponents/ErrorBottomSheet.tsx b/src/frontend/sharedComponents/ErrorBottomSheet.tsx index 14a2f3429..28eda9359 100644 --- a/src/frontend/sharedComponents/ErrorBottomSheet.tsx +++ b/src/frontend/sharedComponents/ErrorBottomSheet.tsx @@ -1,6 +1,6 @@ import * as React from 'react'; import * as Sentry from '@sentry/react-native'; -import {defineMessages, useIntl} from 'react-intl'; +import {MessageDescriptor, defineMessages, useIntl} from 'react-intl'; import {ActionButtonConfig} from './BottomSheetModal/Content'; import { @@ -29,10 +29,12 @@ type ErrorModalProps = { error: Error | null; clearError: () => void; tryAgain?: () => unknown; + title?: MessageDescriptor; + description?: MessageDescriptor; }; export const ErrorBottomSheet = (props: ErrorModalProps) => { - const {error, clearError, tryAgain} = props; + const {error, clearError, tryAgain, title, description} = props; const {formatMessage} = useIntl(); const {openSheet, sheetRef, isOpen, closeSheet} = useBottomSheetModal({ @@ -75,7 +77,8 @@ export const ErrorBottomSheet = (props: ErrorModalProps) => { isOpen={isOpen}> } - title={formatMessage(m.somethingWrong)} + title={formatMessage(title || m.somethingWrong)} + description={description ? formatMessage(description) : undefined} buttonConfigs={buttonConfigs} /> From 173b32a0bd0aac912072e7a426d8fb7b156ce5a3 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 10:33:20 -0400 Subject: [PATCH 18/44] Undoing recent changes --- .../screens/Audio/CreateRecording/RecordingDone.tsx | 6 ------ src/frontend/screens/Audio/CreateRecording/index.tsx | 6 +----- 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 5a2b155f9..340cedb91 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -15,7 +15,6 @@ import ErrorIcon from '../../../images/Error.svg'; import SuccessIcon from '../../../images/GreenCheck.svg'; import {Playback} from '../../../sharedComponents/Playback'; import {useDraftObservation} from '../../../hooks/useDraftObservation'; -import {useBottomSheetModal as useGorhamBottomSheet} from '@gorhom/bottom-sheet'; const m = defineMessages({ deleteBottomSheetTitle: { @@ -88,8 +87,6 @@ export function RecordingDone({ openOnMount: false, }); - const {dismissAll} = useGorhamBottomSheet(); - useEffect(() => { navigation.setOptions({ headerShown: true, @@ -137,9 +134,6 @@ export function RecordingDone({ icon: , onPress: () => { closeSheet(); - if (!sheetRef.current) { - dismissAll(); - } onDelete(); }, variation: 'filled', diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index c899f0635..93259ee07 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -47,12 +47,8 @@ export function CreateRecording() { uri={uri || ''} duration={status?.durationMillis || 0} onDelete={() => { + navigation.goBack(); reset(); - if (navigation.canGoBack()) { - navigation.goBack(); - } else { - navigation.navigate('ObservationCreate'); - } }} onRecordAnother={reset} /> From 9dd91fa8b0f0a9ccaa9359380054994f11168363 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 10:52:33 -0400 Subject: [PATCH 19/44] Adjusts function definitions and calls at runtime --- .../Audio/CreateRecording/RecordingDone.tsx | 14 +++++++------- .../screens/Audio/CreateRecording/index.tsx | 2 +- .../Audio/PermissionAudioBottomSheetContent.tsx | 8 ++------ 3 files changed, 10 insertions(+), 14 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 340cedb91..0029e92ce 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -109,6 +109,11 @@ export function RecordingDone({ }); }, [navigation, addAudioRecording, duration, uri, openSheet]); + const handleDelete = () => { + closeSheet(); + onDelete(); + }; + const handleReturnToEditor = () => { closeSheet(); navigation.navigate('ObservationCreate'); @@ -132,18 +137,13 @@ export function RecordingDone({ dangerous: true, text: t(m.deleteBottomSheetPrimaryButtonText), icon: , - onPress: () => { - closeSheet(); - onDelete(); - }, + onPress: handleDelete, variation: 'filled', }, { variation: 'outlined', text: t(m.deleteBottomSheetSecondaryButtonText), - onPress: () => { - closeSheet(); - }, + onPress: closeSheet, }, ]} /> diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 93259ee07..6e2c76a49 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -47,8 +47,8 @@ export function CreateRecording() { uri={uri || ''} duration={status?.durationMillis || 0} onDelete={() => { - navigation.goBack(); reset(); + navigation.goBack(); }} onRecordAnother={reset} /> diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index 287dd5df9..eb302bc72 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -71,14 +71,10 @@ export const PermissionAudioBottomSheetContent: FC< }; const onPressActionButton = !permissionResponse - ? async () => { - await handleRequestPermission(); - } + ? handleRequestPermission : permissionResponse.status === 'denied' ? handleOpenSettings - : async () => { - await handleRequestPermission(); - }; + : handleRequestPermission; const actionButtonText = !permissionResponse ? t(m.allowButtonText) : permissionResponse.status === 'denied' From 15aa90bd25559ba9542e4868ef949c69c1e938e3 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 11:19:54 -0400 Subject: [PATCH 20/44] Passing on dismiss --- src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx | 1 + .../screens/Audio/PermissionAudioBottomSheetContent.tsx | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 0029e92ce..0b15f2fc9 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -205,6 +205,7 @@ export function RecordingDone({ /> {renderModalContent()} diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index eb302bc72..1bee50bd9 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -61,8 +61,8 @@ export const PermissionAudioBottomSheetContent: FC< const response = await Audio.requestPermissionsAsync(); setPermissionResponse(response); if (response.status === 'granted') { - closeSheet(); navigation.navigate('Audio'); + closeSheet(); } else if (response.status === 'denied' && response.canAskAgain) { closeSheet(); } else if (response.status === 'denied' && !response.canAskAgain) { From 7a7218e50f06f9358082a8ee166f6d6ba5a7a805 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 11:27:10 -0400 Subject: [PATCH 21/44] Removing go back --- src/frontend/screens/Audio/CreateRecording/index.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 6e2c76a49..9304b7855 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -48,7 +48,7 @@ export function CreateRecording() { duration={status?.durationMillis || 0} onDelete={() => { reset(); - navigation.goBack(); + navigation.navigate('ObservationCreate'); }} onRecordAnother={reset} /> From 42827b255bebe5406d745fadf8c4389fdbf48a2e Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 11:39:07 -0400 Subject: [PATCH 22/44] Trying removal of back handler --- .../screens/Audio/CreateRecording/index.tsx | 28 +++++++++---------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 9304b7855..b2020bf68 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -13,20 +13,20 @@ export function CreateRecording() { const currentState = status?.isRecording ? 'active' : uri ? 'done' : 'idle'; - useEffect(() => { - if (currentState === 'active' || currentState === 'done') { - const onBackPress = () => { - return true; - }; - const backHandler = BackHandler.addEventListener( - 'hardwareBackPress', - onBackPress, - ); - return () => { - backHandler.remove(); - }; - } - }, [currentState]); + // useEffect(() => { + // if (currentState === 'active' || currentState === 'done') { + // const onBackPress = () => { + // return true; + // }; + // const backHandler = BackHandler.addEventListener( + // 'hardwareBackPress', + // onBackPress, + // ); + // return () => { + // backHandler.remove(); + // }; + // } + // }, [currentState]); if (currentState === 'idle') { return ; From 3d93b666f95c23a177c94eb3ee075b0abd1118d5 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 12:39:50 -0400 Subject: [PATCH 23/44] Trying some fixes --- .../Audio/CreateRecording/RecordingDone.tsx | 44 +++++++++---------- .../screens/Audio/CreateRecording/index.tsx | 28 ++++++------ src/frontend/sharedComponents/ActionRow.tsx | 4 +- 3 files changed, 36 insertions(+), 40 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 0b15f2fc9..5c8f3ddf3 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -126,29 +126,7 @@ export function RecordingDone({ }; const renderModalContent = () => { - if (modalContentType === 'delete') { - return ( - } - title={t(m.deleteBottomSheetTitle)} - description={t(m.deleteBottomSheetDescription)} - buttonConfigs={[ - { - dangerous: true, - text: t(m.deleteBottomSheetPrimaryButtonText), - icon: , - onPress: handleDelete, - variation: 'filled', - }, - { - variation: 'outlined', - text: t(m.deleteBottomSheetSecondaryButtonText), - onPress: closeSheet, - }, - ]} - /> - ); - } else if (modalContentType === 'success') { + if (modalContentType === 'success') { const description = ( @@ -208,7 +186,25 @@ export function RecordingDone({ onDismiss={closeSheet} isOpen={isModalOpen} fullScreen={modalContentType === 'success'}> - {renderModalContent()} + } + title={t(m.deleteBottomSheetTitle)} + description={t(m.deleteBottomSheetDescription)} + buttonConfigs={[ + { + dangerous: true, + text: t(m.deleteBottomSheetPrimaryButtonText), + icon: , + onPress: handleDelete, + variation: 'filled', + }, + { + variation: 'outlined', + text: t(m.deleteBottomSheetSecondaryButtonText), + onPress: closeSheet, + }, + ]} + /> ); diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index b2020bf68..9304b7855 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -13,20 +13,20 @@ export function CreateRecording() { const currentState = status?.isRecording ? 'active' : uri ? 'done' : 'idle'; - // useEffect(() => { - // if (currentState === 'active' || currentState === 'done') { - // const onBackPress = () => { - // return true; - // }; - // const backHandler = BackHandler.addEventListener( - // 'hardwareBackPress', - // onBackPress, - // ); - // return () => { - // backHandler.remove(); - // }; - // } - // }, [currentState]); + useEffect(() => { + if (currentState === 'active' || currentState === 'done') { + const onBackPress = () => { + return true; + }; + const backHandler = BackHandler.addEventListener( + 'hardwareBackPress', + onBackPress, + ); + return () => { + backHandler.remove(); + }; + } + }, [currentState]); if (currentState === 'idle') { return ; diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index ffe03d53b..62d2bacb8 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -58,14 +58,14 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { navigation.navigate('ObservationFields', {question: 1}); }; - const handleAudioPress = useCallback(async () => { + const handleAudioPress = async () => { const {status} = await Audio.getPermissionsAsync(); if (status === 'granted') { navigation.navigate('Audio'); } else { openAudioPermissionSheet(); } - }, [navigation, openAudioPermissionSheet]); + }; const bottomSheetItems = [ { From 656d0e5ac032d0ed1f5c23344e1744a2cc0e2a02 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 12:47:16 -0400 Subject: [PATCH 24/44] Testing out sheet sizes --- src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 5c8f3ddf3..4a9b5ad0f 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -184,8 +184,7 @@ export function RecordingDone({ + isOpen={isModalOpen}> } title={t(m.deleteBottomSheetTitle)} From 24e4db1dbecf2b130cae8508760774cf5a66db74 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 12:58:03 -0400 Subject: [PATCH 25/44] Trying full size --- src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 4a9b5ad0f..85c47d3da 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -184,7 +184,8 @@ export function RecordingDone({ + isOpen={isModalOpen} + fullScreen> } title={t(m.deleteBottomSheetTitle)} From 36ddb91fee0bf1522dd055640ac1c7769f7f949e Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 13:28:17 -0400 Subject: [PATCH 26/44] Messing with full screen. --- .../Audio/CreateRecording/RecordingDone.tsx | 46 ++++++++++--------- .../BottomSheetModal/Content.tsx | 2 +- 2 files changed, 26 insertions(+), 22 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 85c47d3da..0b15f2fc9 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -126,7 +126,29 @@ export function RecordingDone({ }; const renderModalContent = () => { - if (modalContentType === 'success') { + if (modalContentType === 'delete') { + return ( + } + title={t(m.deleteBottomSheetTitle)} + description={t(m.deleteBottomSheetDescription)} + buttonConfigs={[ + { + dangerous: true, + text: t(m.deleteBottomSheetPrimaryButtonText), + icon: , + onPress: handleDelete, + variation: 'filled', + }, + { + variation: 'outlined', + text: t(m.deleteBottomSheetSecondaryButtonText), + onPress: closeSheet, + }, + ]} + /> + ); + } else if (modalContentType === 'success') { const description = ( @@ -185,26 +207,8 @@ export function RecordingDone({ ref={sheetRef} onDismiss={closeSheet} isOpen={isModalOpen} - fullScreen> - } - title={t(m.deleteBottomSheetTitle)} - description={t(m.deleteBottomSheetDescription)} - buttonConfigs={[ - { - dangerous: true, - text: t(m.deleteBottomSheetPrimaryButtonText), - icon: , - onPress: handleDelete, - variation: 'filled', - }, - { - variation: 'outlined', - text: t(m.deleteBottomSheetSecondaryButtonText), - onPress: closeSheet, - }, - ]} - /> + fullScreen={modalContentType === 'success'}> + {renderModalContent()} ); diff --git a/src/frontend/sharedComponents/BottomSheetModal/Content.tsx b/src/frontend/sharedComponents/BottomSheetModal/Content.tsx index e7340d92c..8d43d0e13 100644 --- a/src/frontend/sharedComponents/BottomSheetModal/Content.tsx +++ b/src/frontend/sharedComponents/BottomSheetModal/Content.tsx @@ -54,7 +54,7 @@ export const Content = ({ titleStyle, }: Props) => { const {window} = useDimensions(); - const {fullScreen} = useBottomSheetModalProperties(); + const fullScreen = true; return ( Date: Thu, 3 Oct 2024 14:44:41 -0400 Subject: [PATCH 27/44] Trying different order --- .../screens/Audio/CreateRecording/RecordingDone.tsx | 10 +++++++--- src/frontend/screens/Audio/CreateRecording/index.tsx | 1 - .../Audio/PermissionAudioBottomSheetContent.tsx | 2 +- src/frontend/sharedComponents/ActionRow.tsx | 4 ++-- .../sharedComponents/BottomSheetModal/Content.tsx | 2 +- 5 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 0b15f2fc9..e76034471 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -109,8 +109,13 @@ export function RecordingDone({ }); }, [navigation, addAudioRecording, duration, uri, openSheet]); - const handleDelete = () => { - closeSheet(); + const handleDelete = async () => { + await closeSheet(); + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.navigate('ObservationCreate'); + } onDelete(); }; @@ -205,7 +210,6 @@ export function RecordingDone({ /> {renderModalContent()} diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 9304b7855..a14ea2e21 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -48,7 +48,6 @@ export function CreateRecording() { duration={status?.durationMillis || 0} onDelete={() => { reset(); - navigation.navigate('ObservationCreate'); }} onRecordAnother={reset} /> diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index 1bee50bd9..eb302bc72 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -61,8 +61,8 @@ export const PermissionAudioBottomSheetContent: FC< const response = await Audio.requestPermissionsAsync(); setPermissionResponse(response); if (response.status === 'granted') { - navigation.navigate('Audio'); closeSheet(); + navigation.navigate('Audio'); } else if (response.status === 'denied' && response.canAskAgain) { closeSheet(); } else if (response.status === 'denied' && !response.canAskAgain) { diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index 62d2bacb8..ffe03d53b 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -58,14 +58,14 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { navigation.navigate('ObservationFields', {question: 1}); }; - const handleAudioPress = async () => { + const handleAudioPress = useCallback(async () => { const {status} = await Audio.getPermissionsAsync(); if (status === 'granted') { navigation.navigate('Audio'); } else { openAudioPermissionSheet(); } - }; + }, [navigation, openAudioPermissionSheet]); const bottomSheetItems = [ { diff --git a/src/frontend/sharedComponents/BottomSheetModal/Content.tsx b/src/frontend/sharedComponents/BottomSheetModal/Content.tsx index 8d43d0e13..e7340d92c 100644 --- a/src/frontend/sharedComponents/BottomSheetModal/Content.tsx +++ b/src/frontend/sharedComponents/BottomSheetModal/Content.tsx @@ -54,7 +54,7 @@ export const Content = ({ titleStyle, }: Props) => { const {window} = useDimensions(); - const fullScreen = true; + const {fullScreen} = useBottomSheetModalProperties(); return ( Date: Thu, 3 Oct 2024 16:21:32 -0400 Subject: [PATCH 28/44] Blocking touch events when modal open. --- .../Audio/CreateRecording/RecordingDone.tsx | 1 + src/frontend/sharedComponents/Playback.tsx | 49 ++++++++++--------- 2 files changed, 28 insertions(+), 22 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index e76034471..5cc2c3ec9 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -198,6 +198,7 @@ export function RecordingDone({ <> { diff --git a/src/frontend/sharedComponents/Playback.tsx b/src/frontend/sharedComponents/Playback.tsx index ad2b127e0..97f109304 100644 --- a/src/frontend/sharedComponents/Playback.tsx +++ b/src/frontend/sharedComponents/Playback.tsx @@ -1,4 +1,5 @@ import React, {ReactNode} from 'react'; +import {View} from 'react-native'; import {Duration} from 'luxon'; import {defineMessages, useIntl} from 'react-intl'; import {ContentWithControls} from '../screens/Audio/ContentWithControls'; @@ -12,10 +13,12 @@ const m = defineMessages({ }); export function Playback({ uri, + isModalOpen, leftControl, rightControl, }: { uri: string; + isModalOpen?: boolean; leftControl?: ReactNode; rightControl?: ReactNode; }) { @@ -23,27 +26,29 @@ export function Playback({ const {duration, currentPosition, isPlaying, stopPlayback, startPlayback} = useAudioPlayback(uri); return ( - - {leftControl && ( - {leftControl} - )} - {isPlaying ? ( - - ) : ( - - )} - {rightControl && ( - {rightControl} - )} - - } - /> + + + {leftControl && ( + {leftControl} + )} + {isPlaying ? ( + + ) : ( + + )} + {rightControl && ( + {rightControl} + )} + + } + /> + ); } From 8018cebc5ff453ae7a9dded0a6044a2d5135bfff Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 16:31:02 -0400 Subject: [PATCH 29/44] Adds flex. --- src/frontend/sharedComponents/Playback.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/frontend/sharedComponents/Playback.tsx b/src/frontend/sharedComponents/Playback.tsx index 97f109304..049a5e8bd 100644 --- a/src/frontend/sharedComponents/Playback.tsx +++ b/src/frontend/sharedComponents/Playback.tsx @@ -26,7 +26,7 @@ export function Playback({ const {duration, currentPosition, isPlaying, stopPlayback, startPlayback} = useAudioPlayback(uri); return ( - + Date: Thu, 3 Oct 2024 16:35:22 -0400 Subject: [PATCH 30/44] Disabling audio press when modal is open. --- src/frontend/sharedComponents/ActionRow.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index ffe03d53b..a52e70cb4 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -59,6 +59,9 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { }; const handleAudioPress = useCallback(async () => { + if (isAudioPermissionSheetOpen) { + return; + } const {status} = await Audio.getPermissionsAsync(); if (status === 'granted') { navigation.navigate('Audio'); From 5a9c2d0de82668b9961f30dde7f50eb81f02edcd Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Thu, 3 Oct 2024 18:02:54 -0400 Subject: [PATCH 31/44] Timeout --- .../Audio/CreateRecording/RecordingDone.tsx | 23 +++++++++++-------- 1 file changed, 13 insertions(+), 10 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 5cc2c3ec9..e4a949004 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -109,14 +109,21 @@ export function RecordingDone({ }); }, [navigation, addAudioRecording, duration, uri, openSheet]); - const handleDelete = async () => { - await closeSheet(); - if (navigation.canGoBack()) { + const handleDelete = () => { + closeSheet(); + onDelete(); + setTimeout(() => { navigation.goBack(); + }, 100); + }; + + const handleDeletePress = () => { + if (isModalOpen) { + return; } else { - navigation.navigate('ObservationCreate'); + setModalContentType('delete'); + openSheet(); } - onDelete(); }; const handleReturnToEditor = () => { @@ -200,11 +207,7 @@ export function RecordingDone({ uri={uri} isModalOpen={isModalOpen} leftControl={ - { - setModalContentType('delete'); - openSheet(); - }}> + } From 836e1e535e1dd66bea794fe6fa4fbdd92b3f9317 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 12:56:54 -0400 Subject: [PATCH 32/44] Trying without timeout --- .../Audio/CreateRecording/RecordingDone.tsx | 21 ++++++++----------- 1 file changed, 9 insertions(+), 12 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index e4a949004..ddae868e3 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -78,12 +78,7 @@ export function RecordingDone({ const [modalContentType, setModalContentType] = useState(null); - const { - sheetRef, - isOpen: isModalOpen, - openSheet, - closeSheet, - } = useBottomSheetModal({ + const {sheetRef, isOpen, openSheet, closeSheet} = useBottomSheetModal({ openOnMount: false, }); @@ -112,13 +107,14 @@ export function RecordingDone({ const handleDelete = () => { closeSheet(); onDelete(); - setTimeout(() => { - navigation.goBack(); - }, 100); + // setTimeout(() => { + // navigation.goBack(); + // }, 100); + navigation.goBack(); }; const handleDeletePress = () => { - if (isModalOpen) { + if (isOpen) { return; } else { setModalContentType('delete'); @@ -205,7 +201,7 @@ export function RecordingDone({ <> @@ -213,8 +209,9 @@ export function RecordingDone({ } /> {renderModalContent()} From 71ef8974688a2f7f093b8a04e88715f734882511 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 13:30:41 -0400 Subject: [PATCH 33/44] Removes references to isOpen --- .../screens/Audio/CreateRecording/RecordingDone.tsx | 9 ++------- src/frontend/sharedComponents/Playback.tsx | 3 +-- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index ddae868e3..b3f36fd85 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -114,12 +114,8 @@ export function RecordingDone({ }; const handleDeletePress = () => { - if (isOpen) { - return; - } else { - setModalContentType('delete'); - openSheet(); - } + setModalContentType('delete'); + openSheet(); }; const handleReturnToEditor = () => { @@ -201,7 +197,6 @@ export function RecordingDone({ <> diff --git a/src/frontend/sharedComponents/Playback.tsx b/src/frontend/sharedComponents/Playback.tsx index 049a5e8bd..97daa37cc 100644 --- a/src/frontend/sharedComponents/Playback.tsx +++ b/src/frontend/sharedComponents/Playback.tsx @@ -13,7 +13,6 @@ const m = defineMessages({ }); export function Playback({ uri, - isModalOpen, leftControl, rightControl, }: { @@ -26,7 +25,7 @@ export function Playback({ const {duration, currentPosition, isPlaying, stopPlayback, startPlayback} = useAudioPlayback(uri); return ( - + Date: Mon, 7 Oct 2024 13:53:34 -0400 Subject: [PATCH 34/44] Tries using on dismiss --- .../Audio/CreateRecording/RecordingDone.tsx | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index b3f36fd85..dc567f38e 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -64,6 +64,7 @@ interface RecordingDoneProps { } type ModalContentType = 'delete' | 'success' | null; +type PendingAction = 'delete' | 'returnToEditor' | 'recordAnother' | null; export function RecordingDone({ duration, @@ -74,6 +75,7 @@ export function RecordingDone({ const {formatMessage: t} = useIntl(); const navigation = useNavigationFromRoot(); const {addAudioRecording} = useDraftObservation(); + const [pendingAction, setPendingAction] = useState(null); const [modalContentType, setModalContentType] = useState(null); @@ -106,11 +108,8 @@ export function RecordingDone({ const handleDelete = () => { closeSheet(); + setPendingAction('delete'); onDelete(); - // setTimeout(() => { - // navigation.goBack(); - // }, 100); - navigation.goBack(); }; const handleDeletePress = () => { @@ -120,13 +119,32 @@ export function RecordingDone({ const handleReturnToEditor = () => { closeSheet(); - navigation.navigate('ObservationCreate'); + setPendingAction('returnToEditor'); }; const handleRecordAnother = async () => { closeSheet(); - await onRecordAnother(); - navigation.navigate('Audio'); + setPendingAction('recordAnother'); + }; + + const onModalDismiss = () => { + console.log( + 'modal dismiss being called with this pending action: ', + pendingAction, + ); + if (pendingAction === 'delete') { + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.navigate('ObservationCreate'); + } + } else if (pendingAction === 'returnToEditor') { + navigation.navigate('ObservationCreate'); + } else if (pendingAction === 'recordAnother') { + onRecordAnother(); + navigation.navigate('Audio'); + } + setPendingAction(null); }; const renderModalContent = () => { @@ -206,7 +224,7 @@ export function RecordingDone({ {renderModalContent()} From c030e857b7a8ccc73905ac6763cb93afe6013c0d Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 14:55:21 -0400 Subject: [PATCH 35/44] Removes console log. Adds switch statement. Tries timeout with permissons. --- .../Audio/CreateRecording/RecordingDone.tsx | 30 ++++++++++--------- .../PermissionAudioBottomSheetContent.tsx | 4 ++- 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index dc567f38e..ab64e1c6e 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -128,21 +128,23 @@ export function RecordingDone({ }; const onModalDismiss = () => { - console.log( - 'modal dismiss being called with this pending action: ', - pendingAction, - ); - if (pendingAction === 'delete') { - if (navigation.canGoBack()) { - navigation.goBack(); - } else { + switch (pendingAction) { + case 'delete': + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.navigate('ObservationCreate'); + } + break; + case 'returnToEditor': navigation.navigate('ObservationCreate'); - } - } else if (pendingAction === 'returnToEditor') { - navigation.navigate('ObservationCreate'); - } else if (pendingAction === 'recordAnother') { - onRecordAnother(); - navigation.navigate('Audio'); + break; + case 'recordAnother': + onRecordAnother(); + navigation.navigate('Audio'); + break; + default: + break; } setPendingAction(null); }; diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index eb302bc72..f8f4ce6e8 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -62,7 +62,9 @@ export const PermissionAudioBottomSheetContent: FC< setPermissionResponse(response); if (response.status === 'granted') { closeSheet(); - navigation.navigate('Audio'); + setTimeout(() => { + navigation.navigate('Audio'); + }, 100); } else if (response.status === 'denied' && response.canAskAgain) { closeSheet(); } else if (response.status === 'denied' && !response.canAskAgain) { From 030a97773ffd6ef1fdbc78bc04bf29216ad76d8a Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 15:48:46 -0400 Subject: [PATCH 36/44] Uses on dismiss in permissions modal --- .../PermissionAudioBottomSheetContent.tsx | 17 ++++------------ src/frontend/sharedComponents/ActionRow.tsx | 20 +++++++++++++------ 2 files changed, 18 insertions(+), 19 deletions(-) diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index f8f4ce6e8..267e9ef18 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -5,7 +5,6 @@ import AudioPermission from '../../images/observationEdit/AudioPermission.svg'; import {BottomSheetModalContent} from '../../sharedComponents/BottomSheetModal'; import {Audio} from 'expo-av'; import {PermissionResponse} from 'expo-modules-core'; -import {NativeRootNavigationProps} from '../../sharedTypes/navigation'; const m = defineMessages({ title: { @@ -37,36 +36,28 @@ const m = defineMessages({ }, }); -type ObservationCreateNavigationProp = - NativeRootNavigationProps<'ObservationCreate'>['navigation']; - interface PermissionAudioBottomSheetContentProps { closeSheet: () => void; - navigation: ObservationCreateNavigationProp; + setPendingAction: (action: 'navigateToAudio' | null) => void; } export const PermissionAudioBottomSheetContent: FC< PermissionAudioBottomSheetContentProps -> = ({closeSheet, navigation}) => { +> = ({closeSheet, setPendingAction}) => { const {formatMessage: t} = useIntl(); const [permissionResponse, setPermissionResponse] = useState(null); const handleOpenSettings = () => { Linking.openSettings(); - closeSheet(); }; const handleRequestPermission = async () => { const response = await Audio.requestPermissionsAsync(); + closeSheet(); setPermissionResponse(response); if (response.status === 'granted') { - closeSheet(); - setTimeout(() => { - navigation.navigate('Audio'); - }, 100); - } else if (response.status === 'denied' && response.canAskAgain) { - closeSheet(); + setPendingAction('navigateToAudio'); } else if (response.status === 'denied' && !response.canAskAgain) { handleOpenSettings(); } diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index a52e70cb4..4eee1aa0d 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -1,4 +1,4 @@ -import React, {useCallback} from 'react'; +import React, {useCallback, useState} from 'react'; import {defineMessages, useIntl} from 'react-intl'; import {ActionTab} from './ActionTab'; import PhotoIcon from '../images/observationEdit/Photo.svg'; @@ -51,6 +51,10 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { openOnMount: false, }); + const [pendingAction, setPendingAction] = useState<'navigateToAudio' | null>( + null, + ); + const handleCameraPress = () => { navigation.navigate('AddPhoto'); }; @@ -59,9 +63,6 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { }; const handleAudioPress = useCallback(async () => { - if (isAudioPermissionSheetOpen) { - return; - } const {status} = await Audio.getPermissionsAsync(); if (status === 'granted') { navigation.navigate('Audio'); @@ -70,6 +71,13 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { } }, [navigation, openAudioPermissionSheet]); + const handleModalDismiss = useCallback(() => { + if (pendingAction === 'navigateToAudio') { + navigation.navigate('Audio'); + setPendingAction(null); + } + }, [pendingAction, navigation]); + const bottomSheetItems = [ { icon: , @@ -102,11 +110,11 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { From 077e8ab254ae86f503796c235e2cee57d48a957a Mon Sep 17 00:00:00 2001 From: ErikSin <67773827+ErikSin@users.noreply.github.com> Date: Mon, 7 Oct 2024 13:59:46 -0700 Subject: [PATCH 37/44] chore: updated styling --- src/frontend/sharedComponents/ErrorBottomSheet.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/frontend/sharedComponents/ErrorBottomSheet.tsx b/src/frontend/sharedComponents/ErrorBottomSheet.tsx index 28eda9359..d5b588537 100644 --- a/src/frontend/sharedComponents/ErrorBottomSheet.tsx +++ b/src/frontend/sharedComponents/ErrorBottomSheet.tsx @@ -76,10 +76,11 @@ export const ErrorBottomSheet = (props: ErrorModalProps) => { onDismiss={closeSheet} isOpen={isOpen}> } + icon={} title={formatMessage(title || m.somethingWrong)} description={description ? formatMessage(description) : undefined} buttonConfigs={buttonConfigs} + descriptionStyle={{fontSize: 16}} /> ); From 8b8b97ba855e81f625ba89707b6b54df214ed85a Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 17:16:26 -0400 Subject: [PATCH 38/44] Uses blur event instead of the onDismiss --- .../Audio/CreateRecording/RecordingDone.tsx | 47 +++++++------------ .../PermissionAudioBottomSheetContent.tsx | 13 +++-- src/frontend/sharedComponents/ActionRow.tsx | 22 ++++----- 3 files changed, 35 insertions(+), 47 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index ab64e1c6e..691a7e38d 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -64,7 +64,6 @@ interface RecordingDoneProps { } type ModalContentType = 'delete' | 'success' | null; -type PendingAction = 'delete' | 'returnToEditor' | 'recordAnother' | null; export function RecordingDone({ duration, @@ -75,7 +74,6 @@ export function RecordingDone({ const {formatMessage: t} = useIntl(); const navigation = useNavigationFromRoot(); const {addAudioRecording} = useDraftObservation(); - const [pendingAction, setPendingAction] = useState(null); const [modalContentType, setModalContentType] = useState(null); @@ -84,6 +82,14 @@ export function RecordingDone({ openOnMount: false, }); + useEffect(() => { + const unsubscribe = navigation.addListener('blur', () => { + closeSheet(); + }); + + return unsubscribe; + }, [navigation, closeSheet]); + useEffect(() => { navigation.setOptions({ headerShown: true, @@ -107,9 +113,12 @@ export function RecordingDone({ }, [navigation, addAudioRecording, duration, uri, openSheet]); const handleDelete = () => { - closeSheet(); - setPendingAction('delete'); onDelete(); + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.navigate('ObservationCreate'); + } }; const handleDeletePress = () => { @@ -118,35 +127,12 @@ export function RecordingDone({ }; const handleReturnToEditor = () => { - closeSheet(); - setPendingAction('returnToEditor'); + navigation.navigate('ObservationCreate'); }; const handleRecordAnother = async () => { - closeSheet(); - setPendingAction('recordAnother'); - }; - - const onModalDismiss = () => { - switch (pendingAction) { - case 'delete': - if (navigation.canGoBack()) { - navigation.goBack(); - } else { - navigation.navigate('ObservationCreate'); - } - break; - case 'returnToEditor': - navigation.navigate('ObservationCreate'); - break; - case 'recordAnother': - onRecordAnother(); - navigation.navigate('Audio'); - break; - default: - break; - } - setPendingAction(null); + onRecordAnother(); + navigation.navigate('Audio'); }; const renderModalContent = () => { @@ -226,7 +212,6 @@ export function RecordingDone({ {renderModalContent()} diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index 267e9ef18..2577965be 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -5,6 +5,7 @@ import AudioPermission from '../../images/observationEdit/AudioPermission.svg'; import {BottomSheetModalContent} from '../../sharedComponents/BottomSheetModal'; import {Audio} from 'expo-av'; import {PermissionResponse} from 'expo-modules-core'; +import {NativeRootNavigationProps} from '../../sharedTypes/navigation'; const m = defineMessages({ title: { @@ -36,20 +37,24 @@ const m = defineMessages({ }, }); +type ObservationCreateNavigationProp = + NativeRootNavigationProps<'ObservationCreate'>['navigation']; + interface PermissionAudioBottomSheetContentProps { closeSheet: () => void; - setPendingAction: (action: 'navigateToAudio' | null) => void; + navigation: ObservationCreateNavigationProp; } export const PermissionAudioBottomSheetContent: FC< PermissionAudioBottomSheetContentProps -> = ({closeSheet, setPendingAction}) => { +> = ({closeSheet, navigation}) => { const {formatMessage: t} = useIntl(); const [permissionResponse, setPermissionResponse] = useState(null); const handleOpenSettings = () => { Linking.openSettings(); + closeSheet(); }; const handleRequestPermission = async () => { @@ -57,7 +62,9 @@ export const PermissionAudioBottomSheetContent: FC< closeSheet(); setPermissionResponse(response); if (response.status === 'granted') { - setPendingAction('navigateToAudio'); + navigation.navigate('Audio'); + } else if (response.status === 'denied' && response.canAskAgain) { + closeSheet(); } else if (response.status === 'denied' && !response.canAskAgain) { handleOpenSettings(); } diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index 4eee1aa0d..d86046419 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useState} from 'react'; +import React, {useCallback, useEffect} from 'react'; import {defineMessages, useIntl} from 'react-intl'; import {ActionTab} from './ActionTab'; import PhotoIcon from '../images/observationEdit/Photo.svg'; @@ -51,9 +51,13 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { openOnMount: false, }); - const [pendingAction, setPendingAction] = useState<'navigateToAudio' | null>( - null, - ); + useEffect(() => { + const unsubscribe = navigation.addListener('blur', () => { + closeAudioPermissionSheet(); + }); + + return unsubscribe; + }, [navigation, closeAudioPermissionSheet]); const handleCameraPress = () => { navigation.navigate('AddPhoto'); @@ -71,13 +75,6 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { } }, [navigation, openAudioPermissionSheet]); - const handleModalDismiss = useCallback(() => { - if (pendingAction === 'navigateToAudio') { - navigation.navigate('Audio'); - setPendingAction(null); - } - }, [pendingAction, navigation]); - const bottomSheetItems = [ { icon: , @@ -110,11 +107,10 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { From fc6cf2b7a05c892f71d018b451db96d4c9cbbe93 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 17:32:26 -0400 Subject: [PATCH 39/44] Passes reset. Removes wrapping. --- .../Audio/CreateRecording/RecordingDone.tsx | 51 ++++++++----------- .../screens/Audio/CreateRecording/index.tsx | 5 +- 2 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 691a7e38d..27c081ee6 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -59,18 +59,12 @@ const m = defineMessages({ interface RecordingDoneProps { duration: number; uri: string; - onDelete: () => void; - onRecordAnother: () => void; + reset: () => void; } type ModalContentType = 'delete' | 'success' | null; -export function RecordingDone({ - duration, - uri, - onDelete, - onRecordAnother, -}: RecordingDoneProps) { +export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { const {formatMessage: t} = useIntl(); const navigation = useNavigationFromRoot(); const {addAudioRecording} = useDraftObservation(); @@ -113,7 +107,8 @@ export function RecordingDone({ }, [navigation, addAudioRecording, duration, uri, openSheet]); const handleDelete = () => { - onDelete(); + closeSheet(); + reset(); if (navigation.canGoBack()) { navigation.goBack(); } else { @@ -131,7 +126,7 @@ export function RecordingDone({ }; const handleRecordAnother = async () => { - onRecordAnother(); + reset(); navigation.navigate('Audio'); }; @@ -175,25 +170,23 @@ export function RecordingDone({ ); return ( - - } - title={t(m.successTitle)} - description={description} - buttonConfigs={[ - { - text: t(m.returnToEditorButtonText), - onPress: handleReturnToEditor, - variation: 'outlined', - }, - { - text: t(m.recordAnotherButtonText), - onPress: handleRecordAnother, - variation: 'filled', - }, - ]} - /> - + } + title={t(m.successTitle)} + description={description} + buttonConfigs={[ + { + text: t(m.returnToEditorButtonText), + onPress: handleReturnToEditor, + variation: 'outlined', + }, + { + text: t(m.recordAnotherButtonText), + onPress: handleRecordAnother, + variation: 'filled', + }, + ]} + /> ); } return null; diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index a14ea2e21..8d5487bed 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -46,10 +46,7 @@ export function CreateRecording() { { - reset(); - }} - onRecordAnother={reset} + reset={reset} /> ); } From 70467b8bf927dac8ea2717a717493f0f3fe1fee7 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 17:51:25 -0400 Subject: [PATCH 40/44] Goes back to using onmodal dismiss --- .../Audio/CreateRecording/RecordingDone.tsx | 42 ++++++++++++------- .../PermissionAudioBottomSheetContent.tsx | 13 ++---- src/frontend/sharedComponents/ActionRow.tsx | 22 ++++++---- 3 files changed, 44 insertions(+), 33 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 27c081ee6..36af20cb3 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -63,11 +63,13 @@ interface RecordingDoneProps { } type ModalContentType = 'delete' | 'success' | null; +type PendingAction = 'delete' | 'returnToEditor' | 'recordAnother' | null; export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { const {formatMessage: t} = useIntl(); const navigation = useNavigationFromRoot(); const {addAudioRecording} = useDraftObservation(); + const [pendingAction, setPendingAction] = useState(null); const [modalContentType, setModalContentType] = useState(null); @@ -76,14 +78,6 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { openOnMount: false, }); - useEffect(() => { - const unsubscribe = navigation.addListener('blur', () => { - closeSheet(); - }); - - return unsubscribe; - }, [navigation, closeSheet]); - useEffect(() => { navigation.setOptions({ headerShown: true, @@ -108,12 +102,8 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { const handleDelete = () => { closeSheet(); + setPendingAction('delete'); reset(); - if (navigation.canGoBack()) { - navigation.goBack(); - } else { - navigation.navigate('ObservationCreate'); - } }; const handleDeletePress = () => { @@ -122,7 +112,8 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { }; const handleReturnToEditor = () => { - navigation.navigate('ObservationCreate'); + closeSheet(); + setPendingAction('recordAnother'); }; const handleRecordAnother = async () => { @@ -130,6 +121,28 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { navigation.navigate('Audio'); }; + const onModalDismiss = () => { + switch (pendingAction) { + case 'delete': + if (navigation.canGoBack()) { + navigation.goBack(); + } else { + navigation.navigate('ObservationCreate'); + } + break; + case 'returnToEditor': + navigation.navigate('ObservationCreate'); + break; + case 'recordAnother': + reset(); + navigation.navigate('Audio'); + break; + default: + break; + } + setPendingAction(null); + }; + const renderModalContent = () => { if (modalContentType === 'delete') { return ( @@ -205,6 +218,7 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { {renderModalContent()} diff --git a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx index 2577965be..267e9ef18 100644 --- a/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx +++ b/src/frontend/screens/Audio/PermissionAudioBottomSheetContent.tsx @@ -5,7 +5,6 @@ import AudioPermission from '../../images/observationEdit/AudioPermission.svg'; import {BottomSheetModalContent} from '../../sharedComponents/BottomSheetModal'; import {Audio} from 'expo-av'; import {PermissionResponse} from 'expo-modules-core'; -import {NativeRootNavigationProps} from '../../sharedTypes/navigation'; const m = defineMessages({ title: { @@ -37,24 +36,20 @@ const m = defineMessages({ }, }); -type ObservationCreateNavigationProp = - NativeRootNavigationProps<'ObservationCreate'>['navigation']; - interface PermissionAudioBottomSheetContentProps { closeSheet: () => void; - navigation: ObservationCreateNavigationProp; + setPendingAction: (action: 'navigateToAudio' | null) => void; } export const PermissionAudioBottomSheetContent: FC< PermissionAudioBottomSheetContentProps -> = ({closeSheet, navigation}) => { +> = ({closeSheet, setPendingAction}) => { const {formatMessage: t} = useIntl(); const [permissionResponse, setPermissionResponse] = useState(null); const handleOpenSettings = () => { Linking.openSettings(); - closeSheet(); }; const handleRequestPermission = async () => { @@ -62,9 +57,7 @@ export const PermissionAudioBottomSheetContent: FC< closeSheet(); setPermissionResponse(response); if (response.status === 'granted') { - navigation.navigate('Audio'); - } else if (response.status === 'denied' && response.canAskAgain) { - closeSheet(); + setPendingAction('navigateToAudio'); } else if (response.status === 'denied' && !response.canAskAgain) { handleOpenSettings(); } diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index d86046419..4eee1aa0d 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -1,4 +1,4 @@ -import React, {useCallback, useEffect} from 'react'; +import React, {useCallback, useState} from 'react'; import {defineMessages, useIntl} from 'react-intl'; import {ActionTab} from './ActionTab'; import PhotoIcon from '../images/observationEdit/Photo.svg'; @@ -51,13 +51,9 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { openOnMount: false, }); - useEffect(() => { - const unsubscribe = navigation.addListener('blur', () => { - closeAudioPermissionSheet(); - }); - - return unsubscribe; - }, [navigation, closeAudioPermissionSheet]); + const [pendingAction, setPendingAction] = useState<'navigateToAudio' | null>( + null, + ); const handleCameraPress = () => { navigation.navigate('AddPhoto'); @@ -75,6 +71,13 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { } }, [navigation, openAudioPermissionSheet]); + const handleModalDismiss = useCallback(() => { + if (pendingAction === 'navigateToAudio') { + navigation.navigate('Audio'); + setPendingAction(null); + } + }, [pendingAction, navigation]); + const bottomSheetItems = [ { icon: , @@ -107,10 +110,11 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { From 797d89ad7f9b01ea15e84f4b88f469900a9785a1 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Mon, 7 Oct 2024 18:09:14 -0400 Subject: [PATCH 41/44] Fixes mistakes in handling modal dismissal --- .../screens/Audio/CreateRecording/RecordingDone.tsx | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index 36af20cb3..b1a551292 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -113,12 +113,13 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { const handleReturnToEditor = () => { closeSheet(); - setPendingAction('recordAnother'); + setPendingAction('returnToEditor'); }; const handleRecordAnother = async () => { + closeSheet(); + setPendingAction('recordAnother'); reset(); - navigation.navigate('Audio'); }; const onModalDismiss = () => { @@ -134,7 +135,6 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { navigation.navigate('ObservationCreate'); break; case 'recordAnother': - reset(); navigation.navigate('Audio'); break; default: From 85fedb12aa8c06d4e304db7e21da5b3210b91a50 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Tue, 8 Oct 2024 09:35:34 -0400 Subject: [PATCH 42/44] Tidies up modal dismissal action --- .../screens/Audio/CreateRecording/RecordingDone.tsx | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx index b1a551292..3644935f8 100644 --- a/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx +++ b/src/frontend/screens/Audio/CreateRecording/RecordingDone.tsx @@ -125,11 +125,7 @@ export function RecordingDone({duration, uri, reset}: RecordingDoneProps) { const onModalDismiss = () => { switch (pendingAction) { case 'delete': - if (navigation.canGoBack()) { - navigation.goBack(); - } else { - navigation.navigate('ObservationCreate'); - } + navigation.goBack(); break; case 'returnToEditor': navigation.navigate('ObservationCreate'); From 47e64e87362877156015f8aae838c0a8c69fd514 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Tue, 8 Oct 2024 10:08:31 -0400 Subject: [PATCH 43/44] Removes unused prop. --- src/frontend/screens/Audio/CreateRecording/index.tsx | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 8d5487bed..5735373ea 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -1,13 +1,11 @@ import React, {useEffect} from 'react'; import {BackHandler} from 'react-native'; -import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes'; import {RecordingActive} from './RecordingActive'; import {RecordingDone} from './RecordingDone'; import {RecordingIdle} from './RecordingIdle'; import {useAudioRecording} from './useAudioRecording'; export function CreateRecording() { - const navigation = useNavigationFromRoot(); const {startRecording, stopRecording, reset, status, uri} = useAudioRecording(); From 8c668678fa6bca62882ce987969b4fc5be7b6fb8 Mon Sep 17 00:00:00 2001 From: Cindy Green Date: Tue, 8 Oct 2024 13:50:44 -0400 Subject: [PATCH 44/44] Updates var name, removes status bar, changes state for permission. --- src/frontend/screens/Audio/CreateRecording/index.tsx | 12 ++++++------ .../Audio/PermissionAudioBottomSheetContent.tsx | 6 +++--- src/frontend/screens/Audio/index.tsx | 2 -- src/frontend/sharedComponents/ActionRow.tsx | 12 +++++------- 4 files changed, 14 insertions(+), 18 deletions(-) diff --git a/src/frontend/screens/Audio/CreateRecording/index.tsx b/src/frontend/screens/Audio/CreateRecording/index.tsx index 5735373ea..2f12fa29e 100644 --- a/src/frontend/screens/Audio/CreateRecording/index.tsx +++ b/src/frontend/screens/Audio/CreateRecording/index.tsx @@ -9,10 +9,10 @@ export function CreateRecording() { const {startRecording, stopRecording, reset, status, uri} = useAudioRecording(); - const currentState = status?.isRecording ? 'active' : uri ? 'done' : 'idle'; + const recordingState = status?.isRecording ? 'active' : uri ? 'done' : 'idle'; useEffect(() => { - if (currentState === 'active' || currentState === 'done') { + if (recordingState === 'active' || recordingState === 'done') { const onBackPress = () => { return true; }; @@ -24,13 +24,13 @@ export function CreateRecording() { backHandler.remove(); }; } - }, [currentState]); + }, [recordingState]); - if (currentState === 'idle') { + if (recordingState === 'idle') { return ; } - if (currentState === 'active') { + if (recordingState === 'active') { return ( void; - setPendingAction: (action: 'navigateToAudio' | null) => void; + setShouldNavigateToAudioTrue: () => void; } export const PermissionAudioBottomSheetContent: FC< PermissionAudioBottomSheetContentProps -> = ({closeSheet, setPendingAction}) => { +> = ({closeSheet, setShouldNavigateToAudioTrue}) => { const {formatMessage: t} = useIntl(); const [permissionResponse, setPermissionResponse] = useState(null); @@ -57,7 +57,7 @@ export const PermissionAudioBottomSheetContent: FC< closeSheet(); setPermissionResponse(response); if (response.status === 'granted') { - setPendingAction('navigateToAudio'); + setShouldNavigateToAudioTrue(); } else if (response.status === 'denied' && !response.canAskAgain) { handleOpenSettings(); } diff --git a/src/frontend/screens/Audio/index.tsx b/src/frontend/screens/Audio/index.tsx index ec1b82eef..600b35377 100644 --- a/src/frontend/screens/Audio/index.tsx +++ b/src/frontend/screens/Audio/index.tsx @@ -3,7 +3,6 @@ import {NativeStackNavigationOptions} from '@react-navigation/native-stack'; import {DARK_GREY, WHITE} from '../../lib/styles'; import {CustomHeaderLeft} from '../../sharedComponents/CustomHeaderLeft'; -import {StatusBar} from 'expo-status-bar'; import {CreateRecording} from './CreateRecording'; export const MAX_RECORDING_DURATION_MS = 5 * 60_000; @@ -11,7 +10,6 @@ export const MAX_RECORDING_DURATION_MS = 5 * 60_000; export function Audio() { return ( <> - ); diff --git a/src/frontend/sharedComponents/ActionRow.tsx b/src/frontend/sharedComponents/ActionRow.tsx index 4eee1aa0d..cb8b5dfb8 100644 --- a/src/frontend/sharedComponents/ActionRow.tsx +++ b/src/frontend/sharedComponents/ActionRow.tsx @@ -51,9 +51,7 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { openOnMount: false, }); - const [pendingAction, setPendingAction] = useState<'navigateToAudio' | null>( - null, - ); + const [shouldNavigateToAudio, setShouldNavigateToAudio] = useState(false); const handleCameraPress = () => { navigation.navigate('AddPhoto'); @@ -72,11 +70,11 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { }, [navigation, openAudioPermissionSheet]); const handleModalDismiss = useCallback(() => { - if (pendingAction === 'navigateToAudio') { + if (shouldNavigateToAudio) { navigation.navigate('Audio'); - setPendingAction(null); + setShouldNavigateToAudio(false); } - }, [pendingAction, navigation]); + }, [shouldNavigateToAudio, navigation]); const bottomSheetItems = [ { @@ -114,7 +112,7 @@ export const ActionsRow = ({fieldRefs}: ActionButtonsProps) => { fullScreen> setShouldNavigateToAudio(true)} />