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:
Marc Rousavy
2021-10-05 12:22:14 +02:00
committed by GitHub
parent a78bff61f9
commit 916278d3ea
23 changed files with 714 additions and 599 deletions

51
example/src/App.tsx Normal file
View 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>
);
}

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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
View File

@@ -0,0 +1,8 @@
export type Routes = {
PermissionsPage: undefined;
CameraPage: undefined;
MediaPage: {
path: string;
type: 'video' | 'photo';
};
};

View File

@@ -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;
};