Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: adds ability to add an audio recording #757

Closed
wants to merge 14 commits into from
Closed
38 changes: 37 additions & 1 deletion messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,21 @@
"description": "Title of dialog that shows when cancelling a new observation",
"message": "Discard observation?"
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.successDescription": {
"message": "Your <bold>{audioRecording}</bold> 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"
},
Expand Down Expand Up @@ -354,12 +369,33 @@
},
"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.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"
},
Expand Down
10 changes: 9 additions & 1 deletion src/frontend/Navigation/Stack/AppScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand Down Expand Up @@ -322,7 +326,11 @@ export const createDefaultScreenGroup = ({
component={HowToLeaveProject}
options={{headerShown: false}}
/>

<RootStack.Screen
name="Audio"
options={audioNavigationOptions}
component={Audio}
/>
{process.env.EXPO_PUBLIC_FEATURE_TEST_DATA_UI && (
<RootStack.Screen
name="CreateTestData"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -23,7 +27,7 @@ const emptyObservation: ClientGeneratedObservation = {

export type DraftObservationSlice = {
photos: Photo[];
audioRecordings: [];
audioRecordings: AudioRecording[];
value: Observation | null | ClientGeneratedObservation;
observationId?: string;
actions: {
Expand Down Expand Up @@ -53,6 +57,7 @@ export type DraftObservationSlice = {
) => void;
updateTags: (tagKey: string, value: Observation['tags'][0]) => void;
updatePreset: (preset: Preset) => void;
addAudioRecording: (audioRecording: AudioRecording) => void;
};
};

Expand Down Expand Up @@ -173,6 +178,10 @@ const draftObservationSlice: StateCreator<DraftObservationSlice> = (
},
});
},
addAudioRecording: recording =>
set({
audioRecordings: [...get().audioRecordings, recording],
}),
},
});

Expand Down
22 changes: 22 additions & 0 deletions src/frontend/hooks/server/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down Expand Up @@ -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<MapeoProjectApi>,
Expand Down
10 changes: 10 additions & 0 deletions src/frontend/hooks/useDraftObservation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -29,6 +30,7 @@ export const useDraftObservation = () => {
updateTags,
updatePreset,
existingObservationToDraft,
addAudioRecording,
} = _usePersistedDraftObservationActions();

const addPhoto = useCallback(
Expand Down Expand Up @@ -92,6 +94,13 @@ export const useDraftObservation = () => {
[deletePersistedPhoto, deletePhotoPromise],
);

const addAudio = useCallback(
(audioRecording: AudioRecording) => {
addAudioRecording(audioRecording);
},
[addAudioRecording],
);

return {
addPhoto,
clearDraft,
Expand All @@ -102,5 +111,6 @@ export const useDraftObservation = () => {
updatePreset,
usePreset,
existingObservationToDraft,
addAudio,
};
};
3 changes: 3 additions & 0 deletions src/frontend/images/PlayArrow.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
10 changes: 9 additions & 1 deletion src/frontend/lib/file-system.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
const posixPath = convertFileUriToPosixPath(fileUri);
return rnUnlink(posixPath);
}

export {ExternalDirectoryPath as EXTERNAL_FILES_DIR};
33 changes: 33 additions & 0 deletions src/frontend/screens/Audio/AnimatedBackground.tsx
Original file line number Diff line number Diff line change
@@ -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<number>;
}) {
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 <Animated.View style={[styles.fill, animatedStyles]} />;
}

const styles = StyleSheet.create({
fill: {
position: 'absolute',
zIndex: -1,
bottom: 0,
width: '100%',
},
});
78 changes: 78 additions & 0 deletions src/frontend/screens/Audio/ContentWithControls.tsx
Original file line number Diff line number Diff line change
@@ -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 (
<ScreenContentWithDock
contentContainerStyle={styles.contentContainer}
dockContainerStyle={styles.dockContainer}
dockContent={controls}>
<View style={styles.container}>
<View style={styles.timerContainer}>
<Text style={styles.timerText}>
{Duration.fromMillis(timeElapsed).toFormat('mm:ss')}
</Text>

{typeof progress === 'number' ? (
<Bar
// Setting to 0 seems to have issues on Android: https://github.com/oblador/react-native-progress/issues/56
progress={progress > 0 ? progress : 0.00000001}
indeterminate={false}
width={null}
color={WHITE}
borderColor="transparent"
borderRadius={0}
borderWidth={0}
unfilledColor={MEDIUM_GREY}
/>
) : (
<View />
)}
</View>
<Text style={styles.message}>{message}</Text>
</View>
</ScreenContentWithDock>
);
}

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',
},
});
Loading
Loading