feat: Add React Native 0.66 support (#490)
* feat: Add React Native 0.66 support * Generate lockfiles * Update Podfile.lock * chore: Migrate from react-native-navigation to react-navigation (#491) * Migrate RNN -> RN * Migrate all screens * Fix get permission status * fix app name * Update AppDelegate.m * Fix Info.plist * Set `UIViewControllerBasedStatusBarAppearance` to `YES` * Only enable `audio` if user granted microphone permission * Update App.tsx * Fix RNGH for Android * Use `navigate` instead of `push` * Fix animation * Upgrade @types/react-native * "Splash" -> "PermissionsPage"
This commit is contained in:
51
example/src/App.tsx
Normal file
51
example/src/App.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
import { NavigationContainer } from '@react-navigation/native';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import { createNativeStackNavigator } from '@react-navigation/native-stack';
|
||||
import { PermissionsPage } from './PermissionsPage';
|
||||
import { MediaPage } from './MediaPage';
|
||||
import { CameraPage } from './CameraPage';
|
||||
import type { Routes } from './Routes';
|
||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';
|
||||
|
||||
const Stack = createNativeStackNavigator<Routes>();
|
||||
|
||||
export function App(): React.ReactElement | null {
|
||||
const [cameraPermission, setCameraPermission] = useState<CameraPermissionStatus>();
|
||||
const [microphonePermission, setMicrophonePermission] = useState<CameraPermissionStatus>();
|
||||
|
||||
useEffect(() => {
|
||||
Camera.getCameraPermissionStatus().then(setCameraPermission);
|
||||
Camera.getMicrophonePermissionStatus().then(setMicrophonePermission);
|
||||
}, []);
|
||||
|
||||
console.log(`Re-rendering Navigator. Camera: ${cameraPermission} | Microphone: ${microphonePermission}`);
|
||||
|
||||
if (cameraPermission == null || microphonePermission == null) {
|
||||
// still loading
|
||||
return null;
|
||||
}
|
||||
|
||||
const showPermissionsPage = cameraPermission !== 'authorized' || microphonePermission === 'not-determined';
|
||||
return (
|
||||
<NavigationContainer>
|
||||
<Stack.Navigator
|
||||
screenOptions={{
|
||||
headerShown: false,
|
||||
statusBarStyle: 'dark',
|
||||
animationTypeForReplace: 'push',
|
||||
}}
|
||||
initialRouteName={showPermissionsPage ? 'PermissionsPage' : 'CameraPage'}>
|
||||
<Stack.Screen name="PermissionsPage" component={PermissionsPage} />
|
||||
<Stack.Screen name="CameraPage" component={CameraPage} />
|
||||
<Stack.Screen
|
||||
name="MediaPage"
|
||||
component={MediaPage}
|
||||
options={{
|
||||
animation: 'none',
|
||||
presentation: 'transparentModal',
|
||||
}}
|
||||
/>
|
||||
</Stack.Navigator>
|
||||
</NavigationContainer>
|
||||
);
|
||||
}
|
@@ -2,7 +2,6 @@ import * as React from 'react';
|
||||
import { useRef, useState, useMemo, useCallback } from 'react';
|
||||
import { StyleSheet, Text, View } from 'react-native';
|
||||
import { PinchGestureHandler, PinchGestureHandlerGestureEvent, TapGestureHandler } from 'react-native-gesture-handler';
|
||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||
import {
|
||||
CameraDeviceFormat,
|
||||
CameraRuntimeError,
|
||||
@@ -14,7 +13,6 @@ import {
|
||||
VideoFile,
|
||||
} from 'react-native-vision-camera';
|
||||
import { Camera, frameRateIncluded } from 'react-native-vision-camera';
|
||||
import { useIsScreenFocussed } from './hooks/useIsScreenFocused';
|
||||
import { CONTENT_SPACING, MAX_ZOOM_FACTOR, SAFE_AREA_PADDING } from './Constants';
|
||||
import Reanimated, { Extrapolate, interpolate, useAnimatedGestureHandler, useAnimatedProps, useSharedValue } from 'react-native-reanimated';
|
||||
import { useEffect } from 'react';
|
||||
@@ -25,6 +23,9 @@ import { PressableOpacity } from 'react-native-pressable-opacity';
|
||||
import MaterialIcon from 'react-native-vector-icons/MaterialCommunityIcons';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import { examplePlugin } from './frame-processors/ExamplePlugin';
|
||||
import type { Routes } from './Routes';
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import { useIsFocused } from '@react-navigation/core';
|
||||
|
||||
const ReanimatedCamera = Reanimated.createAnimatedComponent(Camera);
|
||||
Reanimated.addWhitelistedNativeProps({
|
||||
@@ -34,14 +35,16 @@ Reanimated.addWhitelistedNativeProps({
|
||||
const SCALE_FULL_ZOOM = 3;
|
||||
const BUTTON_SIZE = 40;
|
||||
|
||||
export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
||||
type Props = NativeStackScreenProps<Routes, 'CameraPage'>;
|
||||
export function CameraPage({ navigation }: Props): React.ReactElement {
|
||||
const camera = useRef<Camera>(null);
|
||||
const [isCameraInitialized, setIsCameraInitialized] = useState(false);
|
||||
const [hasMicrophonePermission, setHasMicrophonePermission] = useState(false);
|
||||
const zoom = useSharedValue(0);
|
||||
const isPressingButton = useSharedValue(false);
|
||||
|
||||
// check if camera page is active
|
||||
const isFocussed = useIsScreenFocussed(componentId);
|
||||
const isFocussed = useIsFocused();
|
||||
const isForeground = useIsForeground();
|
||||
const isActive = isFocussed && isForeground;
|
||||
|
||||
@@ -133,18 +136,16 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
||||
console.log('Camera initialized!');
|
||||
setIsCameraInitialized(true);
|
||||
}, []);
|
||||
const onMediaCaptured = useCallback(async (media: PhotoFile | VideoFile, type: 'photo' | 'video') => {
|
||||
console.log(`Media captured! ${JSON.stringify(media)}`);
|
||||
await Navigation.showModal({
|
||||
component: {
|
||||
name: 'MediaPage',
|
||||
passProps: {
|
||||
type: type,
|
||||
path: media.path,
|
||||
},
|
||||
},
|
||||
});
|
||||
}, []);
|
||||
const onMediaCaptured = useCallback(
|
||||
(media: PhotoFile | VideoFile, type: 'photo' | 'video') => {
|
||||
console.log(`Media captured! ${JSON.stringify(media)}`);
|
||||
navigation.navigate('MediaPage', {
|
||||
path: media.path,
|
||||
type: type,
|
||||
});
|
||||
},
|
||||
[navigation],
|
||||
);
|
||||
const onFlipCameraPressed = useCallback(() => {
|
||||
setCameraPosition((p) => (p === 'back' ? 'front' : 'back'));
|
||||
}, []);
|
||||
@@ -165,6 +166,10 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
||||
// Run everytime the neutralZoomScaled value changes. (reset zoom when device changes)
|
||||
zoom.value = neutralZoom;
|
||||
}, [neutralZoom, zoom]);
|
||||
|
||||
useEffect(() => {
|
||||
Camera.getMicrophonePermissionStatus().then((status) => setHasMicrophonePermission(status === 'authorized'));
|
||||
}, []);
|
||||
//#endregion
|
||||
|
||||
//#region Pinch to Zoom Gesture
|
||||
@@ -223,7 +228,7 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
||||
animatedProps={cameraAnimatedProps}
|
||||
photo={true}
|
||||
video={true}
|
||||
audio={true}
|
||||
audio={hasMicrophonePermission}
|
||||
frameProcessor={device.supportsParallelVideoProcessing ? frameProcessor : undefined}
|
||||
frameProcessorFps={1}
|
||||
onFrameProcessorPerformanceSuggestionAvailable={onFrameProcessorSuggestionAvailable}
|
||||
@@ -279,7 +284,7 @@ export const CameraPage: NavigationFunctionComponent = ({ componentId }) => {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@@ -1,10 +1,8 @@
|
||||
import React, { useCallback, useMemo, useState } from 'react';
|
||||
import { StyleSheet, View, Image, ActivityIndicator, PermissionsAndroid, Platform } from 'react-native';
|
||||
import { Navigation, NavigationFunctionComponent, OptionsModalPresentationStyle } from 'react-native-navigation';
|
||||
import Video, { LoadError, OnLoadData } from 'react-native-video';
|
||||
import { SAFE_AREA_PADDING } from './Constants';
|
||||
import { useIsForeground } from './hooks/useIsForeground';
|
||||
import { useIsScreenFocussed } from './hooks/useIsScreenFocused';
|
||||
import { PressableOpacity } from 'react-native-pressable-opacity';
|
||||
import IonIcon from 'react-native-vector-icons/Ionicons';
|
||||
import { Alert } from 'react-native';
|
||||
@@ -12,11 +10,9 @@ import CameraRoll from '@react-native-community/cameraroll';
|
||||
import { StatusBarBlurBackground } from './views/StatusBarBlurBackground';
|
||||
import type { NativeSyntheticEvent } from 'react-native';
|
||||
import type { ImageLoadEventData } from 'react-native';
|
||||
|
||||
interface MediaProps {
|
||||
path: string;
|
||||
type: 'video' | 'photo';
|
||||
}
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import type { Routes } from './Routes';
|
||||
import { useIsFocused } from '@react-navigation/core';
|
||||
|
||||
const requestSavePermission = async (): Promise<boolean> => {
|
||||
if (Platform.OS !== 'android') return true;
|
||||
@@ -34,16 +30,18 @@ const requestSavePermission = async (): Promise<boolean> => {
|
||||
const isVideoOnLoadEvent = (event: OnLoadData | NativeSyntheticEvent<ImageLoadEventData>): event is OnLoadData =>
|
||||
'duration' in event && 'naturalSize' in event;
|
||||
|
||||
export const MediaPage: NavigationFunctionComponent<MediaProps> = ({ componentId, type, path }) => {
|
||||
type Props = NativeStackScreenProps<Routes, 'MediaPage'>;
|
||||
export function MediaPage({ navigation, route }: Props): React.ReactElement {
|
||||
const { path, type } = route.params;
|
||||
const [hasMediaLoaded, setHasMediaLoaded] = useState(false);
|
||||
const isForeground = useIsForeground();
|
||||
const isScreenFocused = useIsScreenFocussed(componentId);
|
||||
const isScreenFocused = useIsFocused();
|
||||
const isVideoPaused = !isForeground || !isScreenFocused;
|
||||
const [savingState, setSavingState] = useState<'none' | 'saving' | 'saved'>('none');
|
||||
|
||||
const onClosePressed = useCallback(() => {
|
||||
Navigation.dismissModal(componentId);
|
||||
}, [componentId]);
|
||||
navigation.goBack();
|
||||
}, [navigation]);
|
||||
|
||||
const onMediaLoad = useCallback((event: OnLoadData | NativeSyntheticEvent<ImageLoadEventData>) => {
|
||||
if (isVideoOnLoadEvent(event)) {
|
||||
@@ -125,24 +123,7 @@ export const MediaPage: NavigationFunctionComponent<MediaProps> = ({ componentId
|
||||
<StatusBarBlurBackground />
|
||||
</View>
|
||||
);
|
||||
};
|
||||
|
||||
MediaPage.options = {
|
||||
modalPresentationStyle: OptionsModalPresentationStyle.overCurrentContext,
|
||||
animations: {
|
||||
showModal: {
|
||||
waitForRender: true,
|
||||
enabled: false,
|
||||
},
|
||||
dismissModal: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
layout: {
|
||||
backgroundColor: 'transparent',
|
||||
componentBackgroundColor: 'transparent',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
container: {
|
||||
|
@@ -1,15 +1,17 @@
|
||||
import type { NativeStackScreenProps } from '@react-navigation/native-stack';
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import { ImageRequireSource, Linking } from 'react-native';
|
||||
|
||||
import { StyleSheet, View, Text, Image } from 'react-native';
|
||||
import { Navigation, NavigationFunctionComponent } from 'react-native-navigation';
|
||||
import { Camera, CameraPermissionStatus } from 'react-native-vision-camera';
|
||||
import { CONTENT_SPACING, SAFE_AREA_PADDING } from './Constants';
|
||||
import type { Routes } from './Routes';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const BANNER_IMAGE = require('../../docs/static/img/11.png') as ImageRequireSource;
|
||||
|
||||
export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
type Props = NativeStackScreenProps<Routes, 'PermissionsPage'>;
|
||||
export function PermissionsPage({ navigation }: Props): React.ReactElement {
|
||||
const [cameraPermissionStatus, setCameraPermissionStatus] = useState<CameraPermissionStatus>('not-determined');
|
||||
const [microphonePermissionStatus, setMicrophonePermissionStatus] = useState<CameraPermissionStatus>('not-determined');
|
||||
|
||||
@@ -18,7 +20,7 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
const permission = await Camera.requestMicrophonePermission();
|
||||
console.log(`Microphone permission status: ${permission}`);
|
||||
|
||||
if (permission === 'denied') Linking.openSettings();
|
||||
if (permission === 'denied') await Linking.openSettings();
|
||||
setMicrophonePermissionStatus(permission);
|
||||
}, []);
|
||||
|
||||
@@ -27,42 +29,13 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
const permission = await Camera.requestCameraPermission();
|
||||
console.log(`Camera permission status: ${permission}`);
|
||||
|
||||
if (permission === 'denied') Linking.openSettings();
|
||||
if (permission === 'denied') await Linking.openSettings();
|
||||
setCameraPermissionStatus(permission);
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
const checkPermissions = async (): Promise<void> => {
|
||||
console.log('Checking Permission status...');
|
||||
const [cameraPermission, microphonePermission] = await Promise.all([
|
||||
Camera.getCameraPermissionStatus(),
|
||||
Camera.getMicrophonePermissionStatus(),
|
||||
]);
|
||||
console.log(`Check: CameraPermission: ${cameraPermission} | MicrophonePermission: ${microphonePermission}`);
|
||||
setCameraPermissionStatus(cameraPermission);
|
||||
setMicrophonePermissionStatus(microphonePermission);
|
||||
};
|
||||
|
||||
checkPermissions();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (cameraPermissionStatus === 'authorized' && microphonePermissionStatus === 'authorized') {
|
||||
Navigation.setRoot({
|
||||
root: {
|
||||
stack: {
|
||||
children: [
|
||||
{
|
||||
component: {
|
||||
name: 'CameraPage',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [cameraPermissionStatus, microphonePermissionStatus, componentId]);
|
||||
if (cameraPermissionStatus === 'authorized' && microphonePermissionStatus === 'authorized') navigation.replace('CameraPage');
|
||||
}, [cameraPermissionStatus, microphonePermissionStatus, navigation]);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
@@ -88,7 +61,7 @@ export const Splash: NavigationFunctionComponent = ({ componentId }) => {
|
||||
</View>
|
||||
</View>
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
const styles = StyleSheet.create({
|
||||
welcome: {
|
8
example/src/Routes.ts
Normal file
8
example/src/Routes.ts
Normal file
@@ -0,0 +1,8 @@
|
||||
export type Routes = {
|
||||
PermissionsPage: undefined;
|
||||
CameraPage: undefined;
|
||||
MediaPage: {
|
||||
path: string;
|
||||
type: 'video' | 'photo';
|
||||
};
|
||||
};
|
@@ -1,39 +0,0 @@
|
||||
import { useCallback, useEffect, useRef, useState } from 'react';
|
||||
import { Navigation } from 'react-native-navigation';
|
||||
|
||||
export const useIsScreenFocussed = (componentId: string): boolean => {
|
||||
const componentStack = useRef<string[]>(['componentId']);
|
||||
const [isFocussed, setIsFocussed] = useState(true);
|
||||
|
||||
const invalidate = useCallback(() => {
|
||||
const last = componentStack.current[componentStack.current.length - 1];
|
||||
setIsFocussed(last === componentId);
|
||||
}, [componentId, setIsFocussed]);
|
||||
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidAppearListener((event) => {
|
||||
if (event.componentType !== 'Component') return;
|
||||
componentStack.current.push(event.componentId);
|
||||
invalidate();
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, [invalidate]);
|
||||
useEffect(() => {
|
||||
const listener = Navigation.events().registerComponentDidDisappearListener((event) => {
|
||||
if (event.componentType !== 'Component') return;
|
||||
// we can't simply use .pop() here because the component might be popped deeper down in the hierarchy.
|
||||
for (let i = componentStack.current.length - 1; i >= 0; i--) {
|
||||
if (componentStack.current[i] === event.componentId) {
|
||||
componentStack.current.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
invalidate();
|
||||
});
|
||||
|
||||
return () => listener.remove();
|
||||
}, [invalidate]);
|
||||
|
||||
return isFocussed;
|
||||
};
|
Reference in New Issue
Block a user