mirror of
https://github.com/laurent22/joplin.git
synced 2026-05-07 20:02:45 +00:00
@@ -2,7 +2,7 @@ import * as React from 'react';
|
|||||||
import { PrimaryButton, SecondaryButton } from '../buttons';
|
import { PrimaryButton, SecondaryButton } from '../buttons';
|
||||||
import { _ } from '@joplin/lib/locale';
|
import { _ } from '@joplin/lib/locale';
|
||||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||||
import { Audio, InterruptionModeIOS } from 'expo-av';
|
import { AudioQuality, getRecordingPermissionsAsync, IOSOutputFormat, requestRecordingPermissionsAsync, setAudioModeAsync, type RecordingOptions, useAudioRecorder as useExpoAudioRecorder, useAudioRecorderState } from 'expo-audio';
|
||||||
import Logger from '@joplin/utils/Logger';
|
import Logger from '@joplin/utils/Logger';
|
||||||
import { OnFileSavedCallback, RecorderState } from './types';
|
import { OnFileSavedCallback, RecorderState } from './types';
|
||||||
import { Platform } from 'react-native';
|
import { Platform } from 'react-native';
|
||||||
@@ -11,7 +11,6 @@ import FsDriverWeb from '../../utils/fs-driver/fs-driver-rn.web';
|
|||||||
import uuid from '@joplin/lib/uuid';
|
import uuid from '@joplin/lib/uuid';
|
||||||
import RecordingControls from './RecordingControls';
|
import RecordingControls from './RecordingControls';
|
||||||
import { Text } from 'react-native-paper';
|
import { Text } from 'react-native-paper';
|
||||||
import { AndroidAudioEncoder, AndroidOutputFormat, IOSAudioQuality, IOSOutputFormat, RecordingOptions } from 'expo-av/build/Audio';
|
|
||||||
import time from '@joplin/lib/time';
|
import time from '@joplin/lib/time';
|
||||||
import { toFileExtension } from '@joplin/lib/mime-utils';
|
import { toFileExtension } from '@joplin/lib/mime-utils';
|
||||||
import { formatMsToDurationCompat, msleep } from '@joplin/utils/time';
|
import { formatMsToDurationCompat, msleep } from '@joplin/utils/time';
|
||||||
@@ -25,23 +24,21 @@ interface Props {
|
|||||||
|
|
||||||
// Modified from the Expo default recording options to create
|
// Modified from the Expo default recording options to create
|
||||||
// .m4a recordings on both Android and iOS (rather than .3gp on Android).
|
// .m4a recordings on both Android and iOS (rather than .3gp on Android).
|
||||||
const recordingOptions = (): RecordingOptions => ({
|
const recordingOptions: RecordingOptions = {
|
||||||
|
extension: '.m4a',
|
||||||
isMeteringEnabled: true,
|
isMeteringEnabled: true,
|
||||||
|
sampleRate: 44100,
|
||||||
|
numberOfChannels: 2,
|
||||||
|
bitRate: 64000,
|
||||||
android: {
|
android: {
|
||||||
extension: '.m4a',
|
extension: '.m4a',
|
||||||
outputFormat: AndroidOutputFormat.MPEG_4,
|
outputFormat: 'mpeg4',
|
||||||
audioEncoder: AndroidAudioEncoder.AAC,
|
audioEncoder: 'aac',
|
||||||
sampleRate: 44100,
|
|
||||||
numberOfChannels: 2,
|
|
||||||
bitRate: 64000,
|
|
||||||
},
|
},
|
||||||
ios: {
|
ios: {
|
||||||
extension: '.m4a',
|
extension: '.m4a',
|
||||||
audioQuality: IOSAudioQuality.MIN,
|
audioQuality: AudioQuality.MIN,
|
||||||
outputFormat: IOSOutputFormat.MPEG4AAC,
|
outputFormat: IOSOutputFormat.MPEG4AAC,
|
||||||
sampleRate: 44100,
|
|
||||||
numberOfChannels: 2,
|
|
||||||
bitRate: 64000,
|
|
||||||
linearPCMBitDepth: 16,
|
linearPCMBitDepth: 16,
|
||||||
linearPCMIsBigEndian: false,
|
linearPCMIsBigEndian: false,
|
||||||
linearPCMIsFloat: false,
|
linearPCMIsFloat: false,
|
||||||
@@ -56,14 +53,16 @@ const recordingOptions = (): RecordingOptions => ({
|
|||||||
].find(type => MediaRecorder.isTypeSupported(type)) ?? 'audio/webm',
|
].find(type => MediaRecorder.isTypeSupported(type)) ?? 'audio/webm',
|
||||||
bitsPerSecond: 128000,
|
bitsPerSecond: 128000,
|
||||||
} : {},
|
} : {},
|
||||||
});
|
};
|
||||||
|
|
||||||
const getRecordingFileName = (extension: string) => {
|
const getRecordingFileName = (extension: string) => {
|
||||||
return `recording-${time.formatDateToLocal(new Date())}${extension}`;
|
return `recording-${time.formatDateToLocal(new Date())}${extension}`;
|
||||||
};
|
};
|
||||||
|
|
||||||
const recordingToSaveData = async (recording: Audio.Recording) => {
|
const recordingToSaveData = async (recordingUri: string|null) => {
|
||||||
let uri = recording.getURI();
|
if (!recordingUri) throw new Error(_('Unable to access the recording file.'));
|
||||||
|
|
||||||
|
let uri = recordingUri;
|
||||||
let type: string|undefined;
|
let type: string|undefined;
|
||||||
let fileName;
|
let fileName;
|
||||||
|
|
||||||
@@ -73,7 +72,7 @@ const recordingToSaveData = async (recording: Audio.Recording) => {
|
|||||||
const fetchResult = await fetch(uri);
|
const fetchResult = await fetch(uri);
|
||||||
const blob = await fetchResult.blob();
|
const blob = await fetchResult.blob();
|
||||||
|
|
||||||
type = recordingOptions().web.mimeType;
|
type = recordingOptions.web.mimeType;
|
||||||
const extension = `.${toFileExtension(type)}`;
|
const extension = `.${toFileExtension(type)}`;
|
||||||
fileName = getRecordingFileName(extension);
|
fileName = getRecordingFileName(extension);
|
||||||
const file = new File([blob], fileName);
|
const file = new File([blob], fileName);
|
||||||
@@ -82,10 +81,9 @@ const recordingToSaveData = async (recording: Audio.Recording) => {
|
|||||||
await (shim.fsDriver() as FsDriverWeb).createReadOnlyVirtualFile(path, file);
|
await (shim.fsDriver() as FsDriverWeb).createReadOnlyVirtualFile(path, file);
|
||||||
uri = path;
|
uri = path;
|
||||||
} else {
|
} else {
|
||||||
const options = recordingOptions();
|
|
||||||
const extension = Platform.select({
|
const extension = Platform.select({
|
||||||
android: options.android.extension,
|
android: recordingOptions.android.extension,
|
||||||
ios: options.ios.extension,
|
ios: recordingOptions.ios.extension,
|
||||||
default: '',
|
default: '',
|
||||||
});
|
});
|
||||||
fileName = getRecordingFileName(extension);
|
fileName = getRecordingFileName(extension);
|
||||||
@@ -95,75 +93,72 @@ const recordingToSaveData = async (recording: Audio.Recording) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const resetAudioMode = async () => {
|
const resetAudioMode = async () => {
|
||||||
await Audio.setAudioModeAsync({
|
await setAudioModeAsync({
|
||||||
// When enabled, iOS may use the small (phone call) speaker
|
allowsRecording: false,
|
||||||
// instead of the default one, so it's disabled when not recording:
|
allowsBackgroundRecording: false,
|
||||||
allowsRecordingIOS: false,
|
playsInSilentMode: false,
|
||||||
playsInSilentModeIOS: false,
|
shouldPlayInBackground: false,
|
||||||
staysActiveInBackground: false,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const useAudioRecorder = (onFileSaved: OnFileSavedCallback, onDismiss: ()=> void) => {
|
const useAudioRecorder = (onFileSaved: OnFileSavedCallback, onDismiss: ()=> void) => {
|
||||||
const [permissionResponse, requestPermissions] = Audio.usePermissions();
|
|
||||||
const [recordingState, setRecordingState] = useState<RecorderState>(RecorderState.Idle);
|
const [recordingState, setRecordingState] = useState<RecorderState>(RecorderState.Idle);
|
||||||
const [error, setError] = useState('');
|
const [error, setError] = useState('');
|
||||||
const [duration, setDuration] = useState(0);
|
const recorder = useExpoAudioRecorder(recordingOptions);
|
||||||
|
const recorderStatus = useAudioRecorderState(recorder, 100);
|
||||||
|
const isRecordingRef = useRef(false);
|
||||||
|
|
||||||
const recordingRef = useRef<Audio.Recording|null>(null);
|
|
||||||
const onStartRecording = useCallback(async () => {
|
const onStartRecording = useCallback(async () => {
|
||||||
try {
|
try {
|
||||||
setRecordingState(RecorderState.Loading);
|
setRecordingState(RecorderState.Loading);
|
||||||
|
setError('');
|
||||||
|
|
||||||
if (permissionResponse?.status !== 'granted') {
|
const permissionResponse = await getRecordingPermissionsAsync();
|
||||||
const response = await requestPermissions();
|
if (permissionResponse.status !== 'granted') {
|
||||||
|
const response = await requestRecordingPermissionsAsync();
|
||||||
if (!response.granted) {
|
if (!response.granted) {
|
||||||
throw new Error(_('Missing permission to record audio.'));
|
throw new Error(_('Missing permission to record audio.'));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Work around "This experience is currently in the background, so the audio session could not be activated"
|
// Work around "This experience is currently in the background, so the audio session could not be activated"
|
||||||
// See https://github.com/expo/expo/issues/21782
|
// See https://github.com/expo/expo/issues/21782
|
||||||
// May be resolved by migrating to expo-audio.
|
|
||||||
await msleep(500);
|
await msleep(500);
|
||||||
}
|
}
|
||||||
|
|
||||||
await Audio.setAudioModeAsync({
|
await setAudioModeAsync({
|
||||||
allowsRecordingIOS: true,
|
allowsRecording: true,
|
||||||
playsInSilentModeIOS: true,
|
allowsBackgroundRecording: true,
|
||||||
staysActiveInBackground: true,
|
playsInSilentMode: true,
|
||||||
|
shouldPlayInBackground: true,
|
||||||
// Fixes an issue where opening a recording in the iOS audio player
|
// Fixes an issue where opening a recording in the iOS audio player
|
||||||
// breaks creating new recordings.
|
// breaks creating new recordings.
|
||||||
// See https://github.com/expo/expo/issues/31152#issuecomment-2341811087
|
// See https://github.com/expo/expo/issues/31152#issuecomment-2341811087
|
||||||
interruptionModeIOS: InterruptionModeIOS.DoNotMix,
|
interruptionMode: 'doNotMix',
|
||||||
});
|
});
|
||||||
|
await recorder.prepareToRecordAsync();
|
||||||
|
isRecordingRef.current = true;
|
||||||
|
recorder.record();
|
||||||
setRecordingState(RecorderState.Recording);
|
setRecordingState(RecorderState.Recording);
|
||||||
const recording = new Audio.Recording();
|
|
||||||
await recording.prepareToRecordAsync(recordingOptions());
|
|
||||||
recording.setOnRecordingStatusUpdate(status => {
|
|
||||||
setDuration(status.durationMillis);
|
|
||||||
});
|
|
||||||
recordingRef.current = recording;
|
|
||||||
await recording.startAsync();
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error('Error starting recording:', error);
|
logger.error('Error starting recording:', error);
|
||||||
setError(`Recording error: ${error}`);
|
setError(`Recording error: ${error}`);
|
||||||
setRecordingState(RecorderState.Error);
|
setRecordingState(RecorderState.Error);
|
||||||
|
|
||||||
void recordingRef.current?.stopAndUnloadAsync();
|
if (isRecordingRef.current) {
|
||||||
recordingRef.current = null;
|
isRecordingRef.current = false;
|
||||||
|
void recorder.stop();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [permissionResponse, requestPermissions]);
|
}, [recorder]);
|
||||||
|
|
||||||
const onStopRecording = useCallback(async () => {
|
const onStopRecording = useCallback(async () => {
|
||||||
const recording = recordingRef.current;
|
|
||||||
recordingRef.current = null;
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
setRecordingState(RecorderState.Processing);
|
setRecordingState(RecorderState.Processing);
|
||||||
await recording.stopAndUnloadAsync();
|
await recorder.stop();
|
||||||
|
isRecordingRef.current = false;
|
||||||
await resetAudioMode();
|
await resetAudioMode();
|
||||||
|
|
||||||
const saveEvent = await recordingToSaveData(recording);
|
const saveEvent = await recordingToSaveData(recorder.uri);
|
||||||
onFileSaved(saveEvent);
|
onFileSaved(saveEvent);
|
||||||
onDismiss();
|
onDismiss();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -171,25 +166,35 @@ const useAudioRecorder = (onFileSaved: OnFileSavedCallback, onDismiss: ()=> void
|
|||||||
setError(`Save error: ${error}`);
|
setError(`Save error: ${error}`);
|
||||||
setRecordingState(RecorderState.Error);
|
setRecordingState(RecorderState.Error);
|
||||||
}
|
}
|
||||||
}, [onFileSaved, onDismiss]);
|
}, [onFileSaved, onDismiss, recorder]);
|
||||||
|
|
||||||
const onStartStopRecording = useCallback(async () => {
|
const onStartStopRecording = useCallback(async () => {
|
||||||
if (recordingState === RecorderState.Idle) {
|
if (recordingState === RecorderState.Idle) {
|
||||||
await onStartRecording();
|
await onStartRecording();
|
||||||
} else if (recordingState === RecorderState.Recording && recordingRef.current) {
|
} else if (recordingState === RecorderState.Recording) {
|
||||||
await onStopRecording();
|
await onStopRecording();
|
||||||
}
|
}
|
||||||
}, [recordingState, onStartRecording, onStopRecording]);
|
}, [recordingState, onStartRecording, onStopRecording]);
|
||||||
|
|
||||||
useEffect(() => () => {
|
useEffect(() => () => {
|
||||||
if (recordingRef.current) {
|
if (isRecordingRef.current) {
|
||||||
void recordingRef.current?.stopAndUnloadAsync();
|
isRecordingRef.current = false;
|
||||||
recordingRef.current = null;
|
|
||||||
void resetAudioMode();
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
return { onStartStopRecording, error, duration, recordingState };
|
const stopRecorderOnCleanup = async () => {
|
||||||
|
try {
|
||||||
|
await recorder.stop();
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn('Error stopping recorder during cleanup:', error);
|
||||||
|
}
|
||||||
|
|
||||||
|
await resetAudioMode();
|
||||||
|
};
|
||||||
|
|
||||||
|
void stopRecorderOnCleanup();
|
||||||
|
}
|
||||||
|
}, [recorder]);
|
||||||
|
|
||||||
|
return { onStartStopRecording, error, duration: recorderStatus.durationMillis, recordingState };
|
||||||
};
|
};
|
||||||
|
|
||||||
const AudioRecordingBanner: React.FC<Props> = props => {
|
const AudioRecordingBanner: React.FC<Props> = props => {
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
PODS:
|
PODS:
|
||||||
- EXAV (16.0.8):
|
|
||||||
- ExpoModulesCore
|
|
||||||
- ReactCommon/turbomodule/core
|
|
||||||
- EXConstants (18.0.13):
|
- EXConstants (18.0.13):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- EXImageLoader (6.0.0):
|
- EXImageLoader (6.0.0):
|
||||||
@@ -34,6 +31,8 @@ PODS:
|
|||||||
- Yoga
|
- Yoga
|
||||||
- ExpoAsset (12.0.12):
|
- ExpoAsset (12.0.12):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
|
- ExpoAudio (1.1.1):
|
||||||
|
- ExpoModulesCore
|
||||||
- ExpoCamera (17.0.10):
|
- ExpoCamera (17.0.10):
|
||||||
- ExpoModulesCore
|
- ExpoModulesCore
|
||||||
- ZXingObjC/OneD
|
- ZXingObjC/OneD
|
||||||
@@ -72,9 +71,9 @@ PODS:
|
|||||||
- ReactNativeDependencies
|
- ReactNativeDependencies
|
||||||
- Yoga
|
- Yoga
|
||||||
- FBLazyVector (0.81.6)
|
- FBLazyVector (0.81.6)
|
||||||
- hermes-engine (0.81.5):
|
- hermes-engine (0.81.6):
|
||||||
- hermes-engine/Pre-built (= 0.81.5)
|
- hermes-engine/Pre-built (= 0.81.6)
|
||||||
- hermes-engine/Pre-built (0.81.5)
|
- hermes-engine/Pre-built (0.81.6)
|
||||||
- JoplinCommonShareExtension (1.0.0)
|
- JoplinCommonShareExtension (1.0.0)
|
||||||
- JoplinRNShareExtension (1.0.0):
|
- JoplinRNShareExtension (1.0.0):
|
||||||
- JoplinCommonShareExtension
|
- JoplinCommonShareExtension
|
||||||
@@ -154,7 +153,7 @@ PODS:
|
|||||||
- React-utils
|
- React-utils
|
||||||
- ReactNativeDependencies
|
- ReactNativeDependencies
|
||||||
- Yoga
|
- Yoga
|
||||||
- React-Core-prebuilt (0.81.5):
|
- React-Core-prebuilt (0.81.6):
|
||||||
- ReactNativeDependencies
|
- ReactNativeDependencies
|
||||||
- React-Core/CoreModulesHeaders (0.81.6):
|
- React-Core/CoreModulesHeaders (0.81.6):
|
||||||
- hermes-engine
|
- hermes-engine
|
||||||
@@ -2134,7 +2133,7 @@ PODS:
|
|||||||
- ReactCommon/turbomodule/core
|
- ReactCommon/turbomodule/core
|
||||||
- ReactNativeDependencies
|
- ReactNativeDependencies
|
||||||
- Yoga
|
- Yoga
|
||||||
- SDWebImage/Core (5.21.5)
|
- SDWebImage/Core (5.21.7)
|
||||||
- SDWebImageWebPCoder (0.15.0):
|
- SDWebImageWebPCoder (0.15.0):
|
||||||
- libwebp (~> 1.0)
|
- libwebp (~> 1.0)
|
||||||
- SDWebImage/Core (~> 5.17)
|
- SDWebImage/Core (~> 5.17)
|
||||||
@@ -2169,11 +2168,11 @@ PODS:
|
|||||||
- ZXingObjC/Core
|
- ZXingObjC/Core
|
||||||
|
|
||||||
DEPENDENCIES:
|
DEPENDENCIES:
|
||||||
- EXAV (from `../node_modules/expo-av/ios`)
|
|
||||||
- EXConstants (from `../node_modules/expo-constants/ios`)
|
- EXConstants (from `../node_modules/expo-constants/ios`)
|
||||||
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
|
- EXImageLoader (from `../node_modules/expo-image-loader/ios`)
|
||||||
- Expo (from `../node_modules/expo`)
|
- Expo (from `../node_modules/expo`)
|
||||||
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
- ExpoAsset (from `../node_modules/expo-asset/ios`)
|
||||||
|
- ExpoAudio (from `../node_modules/expo-audio/ios`)
|
||||||
- ExpoCamera (from `../node_modules/expo-camera/ios`)
|
- ExpoCamera (from `../node_modules/expo-camera/ios`)
|
||||||
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
- ExpoFileSystem (from `../node_modules/expo-file-system/ios`)
|
||||||
- ExpoFont (from `../node_modules/expo-font/ios`)
|
- ExpoFont (from `../node_modules/expo-font/ios`)
|
||||||
@@ -2293,8 +2292,6 @@ SPEC REPOS:
|
|||||||
- ZXingObjC
|
- ZXingObjC
|
||||||
|
|
||||||
EXTERNAL SOURCES:
|
EXTERNAL SOURCES:
|
||||||
EXAV:
|
|
||||||
:path: "../node_modules/expo-av/ios"
|
|
||||||
EXConstants:
|
EXConstants:
|
||||||
:path: "../node_modules/expo-constants/ios"
|
:path: "../node_modules/expo-constants/ios"
|
||||||
EXImageLoader:
|
EXImageLoader:
|
||||||
@@ -2303,6 +2300,8 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/expo"
|
:path: "../node_modules/expo"
|
||||||
ExpoAsset:
|
ExpoAsset:
|
||||||
:path: "../node_modules/expo-asset/ios"
|
:path: "../node_modules/expo-asset/ios"
|
||||||
|
ExpoAudio:
|
||||||
|
:path: "../node_modules/expo-audio/ios"
|
||||||
ExpoCamera:
|
ExpoCamera:
|
||||||
:path: "../node_modules/expo-camera/ios"
|
:path: "../node_modules/expo-camera/ios"
|
||||||
ExpoFileSystem:
|
ExpoFileSystem:
|
||||||
@@ -2522,11 +2521,11 @@ EXTERNAL SOURCES:
|
|||||||
:path: "../node_modules/react-native/ReactCommon/yoga"
|
:path: "../node_modules/react-native/ReactCommon/yoga"
|
||||||
|
|
||||||
SPEC CHECKSUMS:
|
SPEC CHECKSUMS:
|
||||||
EXAV: b60fcf142fae6684d295bc28cd7cfcb3335570ea
|
|
||||||
EXConstants: fce59a631a06c4151602843667f7cfe35f81e271
|
EXConstants: fce59a631a06c4151602843667f7cfe35f81e271
|
||||||
EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05
|
EXImageLoader: 189e3476581efe3ad4d1d3fb4735b7179eb26f05
|
||||||
Expo: 04993fbd7b06dc98ffac58da8847298470dc3db1
|
Expo: 04993fbd7b06dc98ffac58da8847298470dc3db1
|
||||||
ExpoAsset: f867e55ceb428aab99e1e8c082b5aee7c159ea18
|
ExpoAsset: f867e55ceb428aab99e1e8c082b5aee7c159ea18
|
||||||
|
ExpoAudio: e4cfe3a2f3317b8487460685385a9867a07fb4fb
|
||||||
ExpoCamera: 6a326deb45ba840749652e4c15198317aa78497e
|
ExpoCamera: 6a326deb45ba840749652e4c15198317aa78497e
|
||||||
ExpoFileSystem: 858a44267a3e6e9057e0888ad7c7cfbf55d52063
|
ExpoFileSystem: 858a44267a3e6e9057e0888ad7c7cfbf55d52063
|
||||||
ExpoFont: 35ac6191ed86bbf56b3ebd2d9154eda9fad5b509
|
ExpoFont: 35ac6191ed86bbf56b3ebd2d9154eda9fad5b509
|
||||||
@@ -2534,7 +2533,7 @@ SPEC CHECKSUMS:
|
|||||||
ExpoLocalAuthentication: 8a31808565da7af926dd9b595e98594d8b1553b6
|
ExpoLocalAuthentication: 8a31808565da7af926dd9b595e98594d8b1553b6
|
||||||
ExpoModulesCore: f3da4f1ab5a8375d0beafab763739dbee8446583
|
ExpoModulesCore: f3da4f1ab5a8375d0beafab763739dbee8446583
|
||||||
FBLazyVector: 14ce6e3675cacb2683ad30272f04274a4ee5b67d
|
FBLazyVector: 14ce6e3675cacb2683ad30272f04274a4ee5b67d
|
||||||
hermes-engine: 9f4dfe93326146a1c99eb535b1cb0b857a3cd172
|
hermes-engine: 7219f6e751ad6ec7f3d7ec121830ee34dae40749
|
||||||
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
|
JoplinCommonShareExtension: a8b60b02704d85a7305627912c0240e94af78db7
|
||||||
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
|
JoplinRNShareExtension: e158a4b53ee0aa9cd3037a16221dc8adbd6f7860
|
||||||
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
libwebp: 02b23773aedb6ff1fd38cec7a77b81414c6842a8
|
||||||
@@ -2546,7 +2545,7 @@ SPEC CHECKSUMS:
|
|||||||
React: 348d1689d8686d034c5b7667dc45de86c6319dd1
|
React: 348d1689d8686d034c5b7667dc45de86c6319dd1
|
||||||
React-callinvoker: 2c3b664f3482f5bc5560ea1edcbbe69748752f08
|
React-callinvoker: 2c3b664f3482f5bc5560ea1edcbbe69748752f08
|
||||||
React-Core: 346787852200a732b187805344b8a350d464e004
|
React-Core: 346787852200a732b187805344b8a350d464e004
|
||||||
React-Core-prebuilt: 02f0ad625ddd47463c009c2d0c5dd35c0d982599
|
React-Core-prebuilt: 721ab014acfaff1e4b8fc0d2f7d6f41ea9a706ed
|
||||||
React-CoreModules: 7e07391a1082d02c37f846a362f7574ab035933c
|
React-CoreModules: 7e07391a1082d02c37f846a362f7574ab035933c
|
||||||
React-cxxreact: c50d278c785792a077a6b357aaabd9e5d09e9c6f
|
React-cxxreact: c50d278c785792a077a6b357aaabd9e5d09e9c6f
|
||||||
React-debug: 1b91785fec02ea76c793ead23bed1528d96b4262
|
React-debug: 1b91785fec02ea76c793ead23bed1528d96b4262
|
||||||
@@ -2635,7 +2634,7 @@ SPEC CHECKSUMS:
|
|||||||
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
RNSecureRandom: b64d263529492a6897e236a22a2c4249aa1b53dc
|
||||||
RNShare: 0e600372fb35783fe30d413efd28d11de2bf6cf0
|
RNShare: 0e600372fb35783fe30d413efd28d11de2bf6cf0
|
||||||
RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522
|
RNSVG: cf9ae78f2edf2988242c71a6392d15ff7dd62522
|
||||||
SDWebImage: e9c98383c7572d713c1a0d7dd2783b10599b9838
|
SDWebImage: e9fc87c1aab89a8ab1bbd74eba378c6f53be8abf
|
||||||
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
SDWebImageWebPCoder: 0e06e365080397465cc73a7a9b472d8a3bd0f377
|
||||||
WhisperVoiceTyping: 343ea840cbde2a5f3508f8b016ebcf1c089179ea
|
WhisperVoiceTyping: 343ea840cbde2a5f3508f8b016ebcf1c089179ea
|
||||||
Yoga: 786fa7d9d2ff6060b4e688062243fa69c323d140
|
Yoga: 786fa7d9d2ff6060b4e688062243fa69c323d140
|
||||||
|
|||||||
@@ -76,14 +76,41 @@ jest.mock('@react-native-clipboard/clipboard', () => {
|
|||||||
return { default: { getString: jest.fn(), setString: jest.fn() } };
|
return { default: { getString: jest.fn(), setString: jest.fn() } };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
jest.doMock('expo-audio', () => {
|
||||||
|
return {
|
||||||
|
AudioQuality: {
|
||||||
|
MIN: 'min',
|
||||||
|
},
|
||||||
|
IOSOutputFormat: {
|
||||||
|
MPEG4AAC: 'mpeg4aac',
|
||||||
|
},
|
||||||
|
getRecordingPermissionsAsync: jest.fn(async () => ({
|
||||||
|
status: 'granted',
|
||||||
|
granted: true,
|
||||||
|
})),
|
||||||
|
requestRecordingPermissionsAsync: jest.fn(async () => ({
|
||||||
|
status: 'granted',
|
||||||
|
granted: true,
|
||||||
|
})),
|
||||||
|
setAudioModeAsync: jest.fn(async () => null),
|
||||||
|
useAudioRecorder: jest.fn(() => ({
|
||||||
|
prepareToRecordAsync: jest.fn(async () => null),
|
||||||
|
record: jest.fn(),
|
||||||
|
stop: jest.fn(async () => null),
|
||||||
|
uri: null,
|
||||||
|
})),
|
||||||
|
useAudioRecorderState: jest.fn(() => ({
|
||||||
|
durationMillis: 0,
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
const emptyMockPackages = [
|
const emptyMockPackages = [
|
||||||
'react-native-share',
|
'react-native-share',
|
||||||
'react-native-file-viewer',
|
'react-native-file-viewer',
|
||||||
'react-native-image-picker',
|
'react-native-image-picker',
|
||||||
'@react-native-documents/picker',
|
'@react-native-documents/picker',
|
||||||
'@joplin/react-native-saf-x',
|
'@joplin/react-native-saf-x',
|
||||||
'expo-av',
|
|
||||||
'expo-av/build/Audio',
|
|
||||||
'expo-image-manipulator',
|
'expo-image-manipulator',
|
||||||
];
|
];
|
||||||
for (const packageName of emptyMockPackages) {
|
for (const packageName of emptyMockPackages) {
|
||||||
|
|||||||
@@ -49,7 +49,7 @@
|
|||||||
"deprecated-react-native-prop-types": "5.0.0",
|
"deprecated-react-native-prop-types": "5.0.0",
|
||||||
"events": "3.3.0",
|
"events": "3.3.0",
|
||||||
"expo": "54.0.31",
|
"expo": "54.0.31",
|
||||||
"expo-av": "16.0.8",
|
"expo-audio": "1.1.1",
|
||||||
"expo-camera": "17.0.10",
|
"expo-camera": "17.0.10",
|
||||||
"expo-image-manipulator": "14.0.8",
|
"expo-image-manipulator": "14.0.8",
|
||||||
"expo-local-authentication": "17.0.8",
|
"expo-local-authentication": "17.0.8",
|
||||||
|
|||||||
+1
-1
@@ -88,7 +88,7 @@
|
|||||||
"browserify",
|
"browserify",
|
||||||
"codemirror",
|
"codemirror",
|
||||||
"cspell",
|
"cspell",
|
||||||
"expo-av", // Must be updated with expo
|
"expo-audio", // Must be updated with expo
|
||||||
"file-loader",
|
"file-loader",
|
||||||
"gradle",
|
"gradle",
|
||||||
"html-webpack-plugin",
|
"html-webpack-plugin",
|
||||||
|
|||||||
@@ -10827,7 +10827,7 @@ __metadata:
|
|||||||
esbuild: "npm:0.27.2"
|
esbuild: "npm:0.27.2"
|
||||||
events: "npm:3.3.0"
|
events: "npm:3.3.0"
|
||||||
expo: "npm:54.0.31"
|
expo: "npm:54.0.31"
|
||||||
expo-av: "npm:16.0.8"
|
expo-audio: "npm:1.1.1"
|
||||||
expo-camera: "npm:17.0.10"
|
expo-camera: "npm:17.0.10"
|
||||||
expo-image-manipulator: "npm:14.0.8"
|
expo-image-manipulator: "npm:14.0.8"
|
||||||
expo-local-authentication: "npm:17.0.8"
|
expo-local-authentication: "npm:17.0.8"
|
||||||
@@ -28797,18 +28797,15 @@ __metadata:
|
|||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
"expo-av@npm:16.0.8":
|
"expo-audio@npm:1.1.1":
|
||||||
version: 16.0.8
|
version: 1.1.1
|
||||||
resolution: "expo-av@npm:16.0.8"
|
resolution: "expo-audio@npm:1.1.1"
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
expo: "*"
|
expo: "*"
|
||||||
|
expo-asset: "*"
|
||||||
react: "*"
|
react: "*"
|
||||||
react-native: "*"
|
react-native: "*"
|
||||||
react-native-web: "*"
|
checksum: 10/b2013e196eb56d6b31f62d82e3918d81626b8d48bd3b9c3b50d8d97b8329de8ec1cb700cb61cc03b637371213aa2de78fec10208e742e8e2a2019dbf25777814
|
||||||
peerDependenciesMeta:
|
|
||||||
react-native-web:
|
|
||||||
optional: true
|
|
||||||
checksum: 10/c274ae6b98e30b673d2dd03119da703a379c01367466022fa1d2dc1b17ce9475711488132d2c7271a88ecda3800c5d03697371761fa8330e40ca0c02f9b951fa
|
|
||||||
languageName: node
|
languageName: node
|
||||||
linkType: hard
|
linkType: hard
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user