diff --git a/messages/en.json b/messages/en.json
index 5976c4616..969332023 100644
--- a/messages/en.json
+++ b/messages/en.json
@@ -354,12 +354,18 @@
},
"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",
"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"
},
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 && (
;
+}) {
+ 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
new file mode 100644
index 000000000..b5d9167d7
--- /dev/null
+++ b/src/frontend/screens/Audio/CreateRecording/index.tsx
@@ -0,0 +1,34 @@
+import React, {useEffect} from 'react';
+import {useNavigationFromRoot} from '../../../hooks/useNavigationWithTypes';
+import {RecordingActive} from './RecordingActive';
+import {RecordingIdle} from './RecordingIdle';
+import {useAudioRecording} from './useAudioRecording';
+
+export function CreateRecording() {
+ const navigation = useNavigationFromRoot();
+ const recordingState = useAudioRecording();
+
+ useEffect(() => {
+ const unsubscribe = navigation.addListener('focus', () => {
+ recordingState.reset().catch(error => {
+ console.error('Error resetting recording:', error);
+ });
+ });
+
+ 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;
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 = {