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: audio recording #381

Closed
wants to merge 88 commits into from
Closed
Show file tree
Hide file tree
Changes from 80 commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
243c622
implement observation sharing
CDFN May 10, 2024
f0d826c
change share text based on attachements amount
CDFN May 10, 2024
cd50715
lang
CDFN May 10, 2024
7ecaed4
Merge branch 'main' into feat/observation-sharing
CDFN May 10, 2024
44ee88c
add query to fetch attachments in base64 format
CDFN May 14, 2024
4aa4c00
ignore promise rejection when user closes sheet
CDFN May 14, 2024
5bdff97
extract url to base64 function to utils
CDFN May 14, 2024
22df49e
adjust urls in share modal to new types
CDFN May 14, 2024
5552226
fix url type
CDFN May 14, 2024
23976a2
lang, use utm format for coordinates while sharing
CDFN May 14, 2024
aa93a33
fix typing, cleanup code
CDFN May 16, 2024
79d5bb7
Add loading state
CDFN May 16, 2024
aba1388
remvoe unused import
CDFN May 16, 2024
8de7da5
Merge branch 'main' into feat/observation-sharing
CDFN May 16, 2024
1a5d1bc
don't use base64 to display images
CDFN May 18, 2024
4660f25
redisign observation edit screen
bohdanprog May 20, 2024
7fba1c1
change translations
bohdanprog May 20, 2024
30f7321
óMerge branch 'main' of github.com:digidem/CoMapeo-mobile into feat/r…
bohdanprog May 20, 2024
5094abd
update icons, create separate components for ActionTab
bohdanprog May 20, 2024
5f6dc4a
update translates
bohdanprog May 20, 2024
5aad33c
Update ActionTab component
bohdanprog May 20, 2024
356995d
change translations in audio button
bohdanprog May 20, 2024
bd46914
update description field edit observation screen
bohdanprog May 20, 2024
e140762
Merge branch 'main' of github.com:digidem/CoMapeo-mobile into feat/re…
bohdanprog May 20, 2024
b966352
lazy fetch base64 urls, minor nitpicks
CDFN May 20, 2024
7972cee
change styles
bohdanprog May 20, 2024
9ac3aad
remove import
bohdanprog May 20, 2024
719c2b4
feat: display photos in observations (#344)
CDFN May 20, 2024
07662a7
remove base64 query as it's not used anymore
CDFN May 20, 2024
9ce0d10
Merge branch 'main' into feat/observation-sharing
CDFN May 20, 2024
bdeb988
fixed problem with Item Button, create custom hook KeyboardListener, …
bohdanprog May 21, 2024
fab435b
Merge branch 'main' into feat/redesign-edit-observation
bohdanprog May 21, 2024
f36ee7c
fixed problem with feature flag
bohdanprog May 21, 2024
50b1a44
Merge branch 'feat/redesign-edit-observation' of github.com:digidem/C…
bohdanprog May 21, 2024
324d17a
Merge branch 'feat/redesign-edit-observation' of github.com:digidem/C…
bohdanprog May 21, 2024
04f3608
resolve conflict
bohdanprog May 21, 2024
935bbe3
Merge remote-tracking branch 'origin/feat/observation-sharing' into f…
bohdanprog May 21, 2024
c39937c
update thubnail components
bohdanprog May 21, 2024
c0259ba
update imports
bohdanprog May 21, 2024
0cf9453
update imports and rename folder with icons
bohdanprog May 21, 2024
c5c6047
Merge branch 'feat/redesign-edit-observation' of github.com:digidem/C…
bohdanprog May 21, 2024
cf03d5f
change structure files, add play icon
bohdanprog May 21, 2024
82abe01
change type of AudioThumbnail
bohdanprog May 21, 2024
86ab823
add color style to audio svg
bohdanprog May 21, 2024
abea494
Merge branch 'main' of github.com:digidem/CoMapeo-mobile into feat/ne…
bohdanprog May 22, 2024
05b9899
Merge branch 'feat/new-ui-edit-observation-atachment' of github.com:d…
bohdanprog May 22, 2024
6cfc1a3
Initial views for audio
CDFN May 22, 2024
9b4bcad
add audio permission check, add translations, add new microphone icon…
bohdanprog May 22, 2024
d29a96f
generate translations
bohdanprog May 22, 2024
c51941a
complete designs
CDFN May 22, 2024
01ec478
Merge remote-tracking branch 'origin/feat/microfone-permission' into …
CDFN May 22, 2024
cc29084
full recording implementation
CDFN May 23, 2024
858bc91
extract audio playback logic to a hook
CDFN May 23, 2024
6e548f1
resolve conflicts
bohdanprog May 23, 2024
ec3a25a
set navigation header color
bohdanprog May 23, 2024
6b20689
cleanup reanimated sharedvalue calculation, cleanup header styles
CDFN May 23, 2024
58b4b7b
extract recording logic to context
CDFN May 23, 2024
c82d563
add animatedTiemrConponent
bohdanprog May 23, 2024
78d0385
Merge branch 'feat/audio-recording-screen' of github.com:digidem/CoMa…
bohdanprog May 23, 2024
a59618e
add animated timer component and animated progress bar
bohdanprog May 23, 2024
2f5371b
add audio stack
bohdanprog May 23, 2024
d8cf90d
useSharedValue -> useDerivedValue
CDFN May 24, 2024
55536dd
stop recording after 5 minutes
CDFN May 24, 2024
45a8aae
create completely separate navigator for audio recording flow
CDFN May 24, 2024
282a201
remove context from global providers in favor of local stack provider
CDFN May 24, 2024
4bf5e43
add delete audio recording bottom sheet
bohdanprog May 27, 2024
e2dfc82
add translations for delete audio recording bottom sheet
bohdanprog May 27, 2024
8823215
add success save audio recording bottom sheet
bohdanprog May 27, 2024
642ff1b
add transtaions for success bottom sheet
bohdanprog May 27, 2024
66fad86
reorgonize file structure
bohdanprog May 27, 2024
02b8134
add share bottomSheet
bohdanprog May 27, 2024
8b07748
add translations for share bottom sheet
bohdanprog May 27, 2024
58060af
add expo config queries plugin
bohdanprog May 27, 2024
171803b
sharing mods
CDFN May 27, 2024
9c6322a
more recording handling
CDFN May 27, 2024
525bc74
cleanup expo intents plugin
CDFN May 27, 2024
4dd503f
pre-review adjustments
CDFN May 29, 2024
0711b05
Merge branch 'main' into feat/audio-recording-screen
CDFN May 29, 2024
873ee52
remvoe unused file (superseded by MediaScrollView)
CDFN May 29, 2024
46a1363
remove unused lib
CDFN May 29, 2024
12db359
more pre-review changes
CDFN May 29, 2024
98e6d9c
remove messages share option
CDFN May 29, 2024
8f8a282
extract audio press logic to separate func
CDFN May 29, 2024
cf91e9f
translation fixes
CDFN May 29, 2024
005bc02
add translation for timer
CDFN May 29, 2024
7315a43
lang
CDFN May 29, 2024
91aa5c5
Merge branch 'main' into feat/audio-recording-screen
CDFN Jun 3, 2024
5f0a0e0
fix formatting
CDFN Jun 3, 2024
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion app.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@
"organization": "awana-digital"
}
],
["./expo-config-plugins/removeExpoInputStyles.js"]
["./expo-config-plugins/removeExpoInputStyles.js"],
["./expo-config-plugins/addShareIntents.js"]
],
"android": {
"versionCode": 1,
Expand Down
58 changes: 58 additions & 0 deletions expo-config-plugins/addShareIntents.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
const {
withAndroidManifest,
createRunOncePlugin,
} = require('expo/config-plugins');

/**
* @type {import('./types').ManifestQueries}
*/

const queries = {
package: [
{
$: {
'android:name': 'com.whatsapp',
},
},
{
$: {
'android:name': 'com.google.android.apps.messaging',
},
},
],
intent: [
{
action: {
$: {
'android:name': 'android.intent.action.VIEW',
},
},
category: {
$: {
'android:name': 'android.intent.category.BROWSABLE',
},
},
},
],
};

/**
* @param {import('@expo/config-plugins').ExportedConfig} config
*/
const withAndroidManifestService = config => {
return withAndroidManifest(config, config => {
config.modResults.manifest = {
...config.modResults.manifest,
//@ts-ignore
queries,
};

return config;
});
};

module.exports = createRunOncePlugin(
withAndroidManifestService,
'withAndroidManifestService',
'1.0.0',
);
39 changes: 39 additions & 0 deletions messages/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,45 @@
"description": "Title of dialog that shows when cancelling a new observation",
"message": "Discard observation?"
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.cancelButtonText": {
"message": "Cancel"
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.deleteButtonText": {
"message": "Delete"
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.deleteDescription": {
"message": "Your <bold>{audioRecording}</bold> will be permanently deleted. This cannot be undone."
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.deleteTitle": {
"message": "Delete?"
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.recordAnother": {
"message": "Record Another"
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.returnToEditor": {
"message": "Return to Editor"
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.successDescription": {
"message": "Your <bold>{audioRecording}</bold> was added."
},
"AudioPlaybackScreen.DeleteAudioRecordingModal.successTitle": {
"message": "Success!"
},
"AudioPlaybackScreen.ShareAudioRecording.Options.messages": {
"message": "Messages"
},
"AudioPlaybackScreen.ShareAudioRecording.Options.more": {
"message": "More"
},
"AudioPlaybackScreen.ShareAudioRecording.Options.whatsApp": {
"message": "WhatsApp"
},
"AudioPlaybackScreen.ShareAudioRecording.Share.title": {
"message": "Audio recording"
},
"AudioPlaybackScreen.ShareAudioRecording.title": {
"message": "Share via"
},
"Modal.DiscardTrack.description": {
"message": "Your Track will not be saved. This cannot be undone."
},
Expand Down
4 changes: 2 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"cheap-ruler": "^3.0.2",
"eventemitter3": "^5.0.1",
"expo": "~50.0.8",
"expo-av": "^13.10.6",
"expo-camera": "~14.1.3",
"expo-crypto": "~12.8.1",
"expo-dev-client": "~3.3.11",
Expand Down Expand Up @@ -98,8 +99,7 @@
"uint8array-extras": "^0.5.0",
"utm": "^1.1.1",
"validate-color": "^2.2.4",
"zustand": "^4.4.6",
"expo-av": "^13.10.6"
"zustand": "^4.4.6"
},
"devDependencies": {
"@babel/core": "^7.20.0",
Expand Down
11 changes: 11 additions & 0 deletions src/frontend/Navigation/Stack/AppScreens.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ import {SaveTrackScreen} from '../../screens/SaveTrack/SaveTrackScreen';
import {ObservationFields} from '../../screens/ObservationFields';
import {LIGHT_GREY} from '../../lib/styles';
import {LanguageSettings} from '../../screens/Settings/AppSettings/LanguageSettings';
import {AudioStack} from './AudioStack.tsx';
import {AudioRecordingContextProvider} from '../../contexts/AudioRecordingContext.tsx';
import {BottomSheetModalProvider} from '@gorhom/bottom-sheet';

export const TAB_BAR_HEIGHT = 70;
Expand Down Expand Up @@ -255,5 +257,14 @@ export const createDefaultScreenGroup = ({
component={LanguageSettings}
options={{headerTitle: intl(LanguageSettings.navTitle)}}
/>
<RootStack.Screen
name="Audio"
options={{headerShown: false}}
children={() => (
<AudioRecordingContextProvider>
<AudioStack />
</AudioRecordingContextProvider>
)}
/>
</RootStack.Group>
);
48 changes: 48 additions & 0 deletions src/frontend/Navigation/Stack/AudioStack.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import {AudioPrepareRecordingScreen} from '../../screens/Audio/AudioPrepareRecordingScreen.tsx';
import {useNavigationFromRoot} from '../../hooks/useNavigationWithTypes.ts';
import NavigationBackButton from '../../images/navigationBackButton.svg';
import {AudioRecordingScreen} from '../../screens/Audio/AudioRecordingScreen.tsx';
import {AudioPlaybackScreen} from '../../screens/Audio/AudioPlaybackScreen.tsx';
import * as React from 'react';
import {createNativeStackNavigator} from '@react-navigation/native-stack';
import {AudioStackParamList} from '../../sharedTypes/navigation.ts';
import {AUDIO_BLACK} from '../../lib/styles.ts';

const Stack = createNativeStackNavigator<AudioStackParamList>();

export const AudioStack = () => {
return (
<Stack.Navigator>
<Stack.Screen
name="PrepareRecording"
component={AudioPrepareRecordingScreen}
options={{
headerStyle: {backgroundColor: AUDIO_BLACK},
headerTransparent: true,
headerTitle: '',
}}
/>
<Stack.Screen
name="Recording"
component={AudioRecordingScreen}
options={{
headerLeft: () => <></>,
headerStyle: {backgroundColor: 'transparent'},
headerTransparent: true,
headerTitle: '',
animation: 'none',
}}
/>
<Stack.Screen
name="Playback"
component={AudioPlaybackScreen}
options={{
headerStyle: {backgroundColor: 'transparent'},
headerTransparent: true,
headerTitle: '',
animation: 'none',
}}
/>
</Stack.Navigator>
);
};
90 changes: 90 additions & 0 deletions src/frontend/contexts/AudioRecordingContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
import {Recording, RecordingStatus} from 'expo-av/build/Audio/Recording';
import React, {createContext, useContext, useState} from 'react';
import {RecordingOptionsPresets} from 'expo-av/src/Audio/RecordingConstants.ts';

interface AudioRecordingContext {
startRecording: () => Promise<void>;
stopRecording: () => Promise<void>;
hasStarted: boolean;
hasFinished: boolean;
timeElapsed: number;
// Available only after hasStarted === true
recording: Recording | null;
}

const AudioRecordingContext = createContext<AudioRecordingContext | null>(null);

const AudioRecordingContextProvider = ({
children,
}: {
children: React.ReactNode;
}) => {
const [timeElapsed, setTimeElapsed] = useState(0);
const [hasStarted, setStarted] = useState(false);
const [hasFinished, setFinished] = useState(false);

const [recording, setRecording] = useState<Recording | null>(null);

const recordingUpdateHandler = (status: RecordingStatus) => {
setTimeElapsed(status.durationMillis);
if (status.isDoneRecording) {
setFinished(true);
setStarted(false);
}
};

const startRecording = async () => {
if (hasStarted) {
console.warn(
'startRecording from AudioRecordingContext called while recording is already in progress',
);
return;
}
const {recording: newRecording, status} = await Recording.createAsync(
RecordingOptionsPresets.HIGH_QUALITY,
);
newRecording.setOnRecordingStatusUpdate(recordingUpdateHandler);
setStarted(status.isRecording);
setFinished(false);
setTimeElapsed(0);
setRecording(newRecording);
};

const stopRecording = async () => {
if (!hasStarted) {
console.warn(
'stopRecording from AudioRecordingContext called while recording has not been started',
);
return;
}
await recording!.stopAndUnloadAsync();
setFinished(true);
setStarted(false);
};
CDFN marked this conversation as resolved.
Show resolved Hide resolved

return (
<AudioRecordingContext.Provider
value={{
startRecording,
stopRecording,
timeElapsed,
hasStarted,
hasFinished,
recording,
}}>
{children}
</AudioRecordingContext.Provider>
);
};

const useAudioRecordingContext = () => {
const context = useContext(AudioRecordingContext);
if (!context) {
throw new Error(
'useAudioRecordingContext must be used within a AudioRecordingContextProvider',
);
}
return context;
};

export {AudioRecordingContextProvider, useAudioRecordingContext};
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import {ClientGeneratedObservation, Position} from '../../../sharedTypes';
import {Observation, Preset} from '@mapeo/schema';
import {usePresetsQuery} from '../../server/presets';
import {matchPreset} from '../../../lib/utils';
import {AudioRecording} from '../../../sharedTypes/audio.ts';

type newDraftProps = {observation: Observation; preset: Preset};
const emptyObservation: ClientGeneratedObservation = {
Expand All @@ -23,12 +24,14 @@ const emptyObservation: ClientGeneratedObservation = {

export type DraftObservationSlice = {
photos: Photo[];
audioRecordings: [];
audioRecordings: AudioRecording[];
value: Observation | null | ClientGeneratedObservation;
observationId?: string;
actions: {
addPhotoPlaceholder: (draftPhotoId: string) => void;
replacePhotoPlaceholderWithPhoto: (photo: DraftPhoto) => void;
addAudioRecording: (audioRecording: AudioRecording) => void;
removeAudioRecording: (uri: string) => void;
// Clear the current draft
clearDraft: () => void;
// Create a new draft observation
Expand Down Expand Up @@ -56,6 +59,14 @@ const draftObservationSlice: StateCreator<DraftObservationSlice> = (
set({photos: [...get().photos, {draftPhotoId, capturing: true}]}),
replacePhotoPlaceholderWithPhoto: draftPhoto =>
replaceDraftPhotos(set, get, draftPhoto),
addAudioRecording: recording =>
set({
audioRecordings: [...get().audioRecordings, recording],
}),
removeAudioRecording: uri =>
set({
audioRecordings: get().audioRecordings.filter(rec => rec.uri !== uri),
}),
clearDraft: () => {
set({
photos: [],
Expand Down
21 changes: 20 additions & 1 deletion src/frontend/hooks/server/media.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,14 @@ import {URL} from 'react-native-url-polyfill';

import {useActiveProject} from '../../contexts/ActiveProjectContext';
import {DraftPhoto} from '../../contexts/PhotoPromiseContext/types';
import {AudioRecording} from '../../sharedTypes/audio.ts';

type SavablePhoto = SetRequired<
Pick<DraftPhoto, 'originalUri' | 'previewUri' | 'thumbnailUri'>,
'originalUri'
>;

export function useCreateBlobMutation(opts: {retry?: number} = {}) {
export function useCreatePhotoBlobMutation(opts: {retry?: number} = {}) {
const project = useActiveProject();

return useMutation({
Expand All @@ -34,6 +35,24 @@ export function useCreateBlobMutation(opts: {retry?: number} = {}) {
});
}

export function useCreateAudioRecordingBlobMutation(
opts: {retry?: number} = {},
) {
const project = useActiveProject();

return useMutation({
retry: opts.retry,
mutationFn: async (recording: AudioRecording) => {
return project.$blobs.create(
{
original: new URL(recording.uri).pathname,
},
{mimeType: 'audio/mp4'},
);
},
});
}

export function useAttachmentUrlQueries(
attachments: Observation['attachments'],
variant: BlobVariant<
Expand Down
Loading
Loading