diff --git a/native/account/registration/registration-navigator.react.js b/native/account/registration/registration-navigator.react.js index 080099317..6fc6f06c8 100644 --- a/native/account/registration/registration-navigator.react.js +++ b/native/account/registration/registration-navigator.react.js @@ -1,96 +1,107 @@ // @flow import { createStackNavigator, type StackNavigationProp, type StackNavigationHelpers, } from '@react-navigation/stack'; import * as React from 'react'; import AvatarSelection from './avatar-selection.react.js'; import ConnectEthereum from './connect-ethereum.react.js'; import CoolOrNerdModeSelection from './cool-or-nerd-mode-selection.react.js'; import EmojiAvatarSelection from './emoji-avatar-selection.react.js'; import ExistingEthereumAccount from './existing-ethereum-account.react.js'; import KeyserverSelection from './keyserver-selection.react.js'; import PasswordSelection from './password-selection.react.js'; import UsernameSelection from './username-selection.react.js'; +import RegistrationUserAvatarCameraModal from '../../media/registration-user-avatar-camera-modal.react.js'; import type { RootNavigationProp } from '../../navigation/root-navigator.react.js'; import { KeyserverSelectionRouteName, CoolOrNerdModeSelectionRouteName, ConnectEthereumRouteName, ExistingEthereumAccountRouteName, UsernameSelectionRouteName, PasswordSelectionRouteName, AvatarSelectionRouteName, EmojiAvatarSelectionRouteName, + RegistrationUserAvatarCameraModalRouteName, type ScreenParamList, type RegistrationParamList, } from '../../navigation/route-names.js'; export type RegistrationNavigationProp< RouteName: $Keys = $Keys, > = StackNavigationProp; const Registration = createStackNavigator< ScreenParamList, RegistrationParamList, StackNavigationHelpers, >(); const screenOptions = { headerTransparent: true, headerBackTitleVisible: false, headerTitle: '', headerTintColor: 'white', headerLeftContainerStyle: { paddingLeft: 12, }, }; +const cameraScreenOptions = { + headerShown: false, +}; + type Props = { +navigation: RootNavigationProp<'Registration'>, ... }; // eslint-disable-next-line no-unused-vars function RegistrationNavigator(props: Props): React.Node { return ( + ); } export default RegistrationNavigator; diff --git a/native/media/camera-modal.react.js b/native/media/camera-modal.react.js index 74a3bf713..16680f8a7 100644 --- a/native/media/camera-modal.react.js +++ b/native/media/camera-modal.react.js @@ -1,1195 +1,1206 @@ // @flow import Icon from '@expo/vector-icons/Ionicons.js'; import invariant from 'invariant'; import * as React from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Platform, Image, Animated, Easing, } from 'react-native'; import { RNCamera } from 'react-native-camera'; import filesystem from 'react-native-fs'; import { PinchGestureHandler, TapGestureHandler, State as GestureState, } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import type { Orientations } from 'react-native-orientation-locker'; import Reanimated, { EasingNode as ReanimatedEasing, } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import { useDispatch } from 'react-redux'; import { pathFromURI, filenameFromPathOrURI } from 'lib/media/file-utils.js'; import { useIsAppForegrounded } from 'lib/shared/lifecycle-utils.js'; import type { PhotoCapture } from 'lib/types/media-types.js'; import type { Dispatch } from 'lib/types/redux-types.js'; import SendMediaButton from './send-media-button.react.js'; +import type { RegistrationNavigationProp } from '../account/registration/registration-navigator.react.js'; import ContentLoading from '../components/content-loading.react.js'; import ConnectedStatusBar from '../connected-status-bar.react.js'; import type { AppNavigationProp } from '../navigation/app-navigator.react.js'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context.js'; -import type { OverlayParamList } from '../navigation/route-names.js'; import { updateDeviceCameraInfoActionType } from '../redux/action-types.js'; import { type DimensionsInfo } from '../redux/dimensions-updater.react.js'; import { useSelector } from '../redux/redux-utils.js'; import { colors } from '../themes/colors.js'; import { type DeviceCameraInfo } from '../types/camera.js'; import type { NativeMethods } from '../types/react-native.js'; import { AnimatedView, type ViewStyle, type AnimatedViewStyle, } from '../types/styles.js'; import { clamp, gestureJustEnded } from '../utils/animation-utils.js'; /* eslint-disable import/no-named-as-default-member */ const { Value, Node, Clock, event, Extrapolate, block, set, call, cond, not, and, or, eq, greaterThan, lessThan, add, sub, multiply, divide, abs, interpolateNode, startClock, stopClock, clockRunning, timing, spring, SpringUtils, } = Reanimated; /* eslint-enable import/no-named-as-default-member */ const maxZoom = 16; const zoomUpdateFactor = (() => { if (Platform.OS === 'ios') { return 0.002; } if (Platform.OS === 'android' && Platform.Version > 26) { return 0.005; } if (Platform.OS === 'android' && Platform.Version > 23) { return 0.01; } return 0.03; })(); const stagingModeAnimationConfig = { duration: 150, easing: ReanimatedEasing.inOut(ReanimatedEasing.ease), }; const sendButtonAnimationConfig = { duration: 150, easing: Easing.inOut(Easing.ease), useNativeDriver: true, }; const indicatorSpringConfig = { ...SpringUtils.makeDefaultConfig(), damping: 0, mass: 0.6, toValue: 1, }; const indicatorTimingConfig = { duration: 500, easing: ReanimatedEasing.out(ReanimatedEasing.ease), toValue: 0, }; function runIndicatorAnimation( // Inputs springClock: Clock, delayClock: Clock, timingClock: Clock, animationRunning: Node, // Outputs scale: Value, opacity: Value, ): Node { const delayStart = new Value(0); const springScale = new Value(0.75); const delayScale = new Value(0); const timingScale = new Value(0.75); const animatedScale = cond( clockRunning(springClock), springScale, cond(clockRunning(delayClock), delayScale, timingScale), ); const lastAnimatedScale = new Value(0.75); const numScaleLoops = new Value(0); const springState = { finished: new Value(1), velocity: new Value(0), time: new Value(0), position: springScale, }; const timingState = { finished: new Value(1), frameTime: new Value(0), time: new Value(0), position: timingScale, }; return block([ cond(not(animationRunning), [ set(springState.finished, 0), set(springState.velocity, 0), set(springState.time, 0), set(springScale, 0.75), set(lastAnimatedScale, 0.75), set(numScaleLoops, 0), set(opacity, 1), startClock(springClock), ]), [ cond( clockRunning(springClock), spring(springClock, springState, indicatorSpringConfig), ), timing(timingClock, timingState, indicatorTimingConfig), ], [ cond( and( greaterThan(animatedScale, 1.2), not(greaterThan(lastAnimatedScale, 1.2)), ), [ set(numScaleLoops, add(numScaleLoops, 1)), cond(greaterThan(numScaleLoops, 1), [ set(springState.finished, 1), stopClock(springClock), set(delayScale, springScale), set(delayStart, delayClock), startClock(delayClock), ]), ], ), set(lastAnimatedScale, animatedScale), ], cond( and( clockRunning(delayClock), greaterThan(delayClock, add(delayStart, 400)), ), [ stopClock(delayClock), set(timingState.finished, 0), set(timingState.frameTime, 0), set(timingState.time, 0), set(timingScale, delayScale), startClock(timingClock), ], ), cond( and(springState.finished, timingState.finished), stopClock(timingClock), ), set(scale, animatedScale), cond(clockRunning(timingClock), set(opacity, clamp(animatedScale, 0, 1))), ]); } type TouchableOpacityInstance = React.AbstractComponent< React.ElementConfig, NativeMethods, >; type BaseProps = { +handlePhotoCapture: (capture: PhotoCapture) => mixed, - +navigation: $Values< - $ObjMapi(K) => AppNavigationProp>, - >, + +navigation: + | AppNavigationProp<'ChatCameraModal'> + | AppNavigationProp<'UserAvatarCameraModal'> + | AppNavigationProp<'ThreadAvatarCameraModal'> + | RegistrationNavigationProp<'RegistrationUserAvatarCameraModal'>, }; type Props = { ...BaseProps, // Redux state +dimensions: DimensionsInfo, +deviceCameraInfo: DeviceCameraInfo, +deviceOrientation: Orientations, +foreground: boolean, // Redux dispatch functions +dispatch: Dispatch, // withOverlayContext +overlayContext: ?OverlayContextType, }; type State = { +zoom: number, +useFrontCamera: boolean, +hasCamerasOnBothSides: boolean, +flashMode: number, +autoFocusPointOfInterest: ?{ x: number, y: number, autoExposure?: boolean, }, +stagingMode: boolean, +pendingPhotoCapture: ?PhotoCapture, }; class CameraModal extends React.PureComponent { camera: ?RNCamera; pinchEvent; pinchHandler = React.createRef(); tapEvent; tapHandler = React.createRef(); animationCode: Node; closeButton: ?React.ElementRef; closeButtonX = new Value(-1); closeButtonY = new Value(-1); closeButtonWidth = new Value(0); closeButtonHeight = new Value(0); photoButton: ?React.ElementRef; photoButtonX = new Value(-1); photoButtonY = new Value(-1); photoButtonWidth = new Value(0); photoButtonHeight = new Value(0); switchCameraButton: ?React.ElementRef; switchCameraButtonX = new Value(-1); switchCameraButtonY = new Value(-1); switchCameraButtonWidth = new Value(0); switchCameraButtonHeight = new Value(0); flashButton: ?React.ElementRef; flashButtonX = new Value(-1); flashButtonY = new Value(-1); flashButtonWidth = new Value(0); flashButtonHeight = new Value(0); focusIndicatorX = new Value(-1); focusIndicatorY = new Value(-1); focusIndicatorScale = new Value(0); focusIndicatorOpacity = new Value(0); cancelIndicatorAnimation = new Value(0); cameraIDsFetched = false; stagingModeProgress = new Value(0); sendButtonProgress = new Animated.Value(0); sendButtonStyle: ViewStyle; overlayStyle: AnimatedViewStyle; constructor(props: Props) { super(props); this.state = { zoom: 0, useFrontCamera: props.deviceCameraInfo.defaultUseFrontCamera, hasCamerasOnBothSides: props.deviceCameraInfo.hasCamerasOnBothSides, flashMode: RNCamera.Constants.FlashMode.off, autoFocusPointOfInterest: undefined, stagingMode: false, pendingPhotoCapture: undefined, }; const sendButtonScale = this.sendButtonProgress.interpolate({ inputRange: [0, 1], outputRange: ([1.1, 1]: number[]), // Flow... }); this.sendButtonStyle = { opacity: this.sendButtonProgress, transform: [{ scale: sendButtonScale }], }; const overlayOpacity = interpolateNode(this.stagingModeProgress, { inputRange: [0, 0.01, 1], outputRange: [0, 0.5, 0], extrapolate: Extrapolate.CLAMP, }); this.overlayStyle = { ...styles.overlay, opacity: overlayOpacity, }; const pinchState = new Value(-1); const pinchScale = new Value(1); this.pinchEvent = event([ { nativeEvent: { state: pinchState, scale: pinchScale, }, }, ]); const tapState = new Value(-1); const tapX = new Value(0); const tapY = new Value(0); this.tapEvent = event([ { nativeEvent: { state: tapState, x: tapX, y: tapY, }, }, ]); this.animationCode = block([ this.zoomAnimationCode(pinchState, pinchScale), this.focusAnimationCode(tapState, tapX, tapY), ]); } zoomAnimationCode(pinchState: Node, pinchScale: Node): Node { const pinchJustEnded = gestureJustEnded(pinchState); const zoomBase = new Value(1); const zoomReported = new Value(1); const currentZoom = interpolateNode(multiply(zoomBase, pinchScale), { inputRange: [1, 8], outputRange: [1, 8], extrapolate: Extrapolate.CLAMP, }); const cameraZoomFactor = interpolateNode(zoomReported, { inputRange: [1, 8], outputRange: [0, 1], extrapolate: Extrapolate.CLAMP, }); const resolvedZoom = cond( eq(pinchState, GestureState.ACTIVE), currentZoom, zoomBase, ); return block([ cond(pinchJustEnded, set(zoomBase, currentZoom)), cond( or( pinchJustEnded, greaterThan( abs(sub(divide(resolvedZoom, zoomReported), 1)), zoomUpdateFactor, ), ), [ set(zoomReported, resolvedZoom), call([cameraZoomFactor], this.updateZoom), ], ), ]); } focusAnimationCode(tapState: Node, tapX: Node, tapY: Node): Node { const lastTapX = new Value(0); const lastTapY = new Value(0); const fingerJustReleased = and( gestureJustEnded(tapState), this.outsideButtons(lastTapX, lastTapY), ); const indicatorSpringClock = new Clock(); const indicatorDelayClock = new Clock(); const indicatorTimingClock = new Clock(); const indicatorAnimationRunning = or( clockRunning(indicatorSpringClock), clockRunning(indicatorDelayClock), clockRunning(indicatorTimingClock), ); return block([ cond(fingerJustReleased, [ call([tapX, tapY], this.focusOnPoint), set(this.focusIndicatorX, tapX), set(this.focusIndicatorY, tapY), stopClock(indicatorSpringClock), stopClock(indicatorDelayClock), stopClock(indicatorTimingClock), ]), cond(this.cancelIndicatorAnimation, [ set(this.cancelIndicatorAnimation, 0), stopClock(indicatorSpringClock), stopClock(indicatorDelayClock), stopClock(indicatorTimingClock), set(this.focusIndicatorOpacity, 0), ]), cond( or(fingerJustReleased, indicatorAnimationRunning), runIndicatorAnimation( indicatorSpringClock, indicatorDelayClock, indicatorTimingClock, indicatorAnimationRunning, this.focusIndicatorScale, this.focusIndicatorOpacity, ), ), set(lastTapX, tapX), set(lastTapY, tapY), ]); } outsideButtons(x: Node, y: Node): Node { const { closeButtonX, closeButtonY, closeButtonWidth, closeButtonHeight, photoButtonX, photoButtonY, photoButtonWidth, photoButtonHeight, switchCameraButtonX, switchCameraButtonY, switchCameraButtonWidth, switchCameraButtonHeight, flashButtonX, flashButtonY, flashButtonWidth, flashButtonHeight, } = this; return and( or( lessThan(x, closeButtonX), greaterThan(x, add(closeButtonX, closeButtonWidth)), lessThan(y, closeButtonY), greaterThan(y, add(closeButtonY, closeButtonHeight)), ), or( lessThan(x, photoButtonX), greaterThan(x, add(photoButtonX, photoButtonWidth)), lessThan(y, photoButtonY), greaterThan(y, add(photoButtonY, photoButtonHeight)), ), or( lessThan(x, switchCameraButtonX), greaterThan(x, add(switchCameraButtonX, switchCameraButtonWidth)), lessThan(y, switchCameraButtonY), greaterThan(y, add(switchCameraButtonY, switchCameraButtonHeight)), ), or( lessThan(x, flashButtonX), greaterThan(x, add(flashButtonX, flashButtonWidth)), lessThan(y, flashButtonY), greaterThan(y, add(flashButtonY, flashButtonHeight)), ), ); } static isActive(props) { const { overlayContext } = props; - invariant(overlayContext, 'CameraModal should have OverlayContext'); + if (!overlayContext) { + return true; + } return !overlayContext.isDismissing; } componentDidMount() { if (CameraModal.isActive(this.props)) { Orientation.unlockAllOrientations(); } } componentWillUnmount() { if (CameraModal.isActive(this.props)) { Orientation.lockToPortrait(); } } componentDidUpdate(prevProps: Props, prevState: State) { const isActive = CameraModal.isActive(this.props); const wasActive = CameraModal.isActive(prevProps); if (isActive && !wasActive) { Orientation.unlockAllOrientations(); } else if (!isActive && wasActive) { Orientation.lockToPortrait(); } if (!this.state.hasCamerasOnBothSides && prevState.hasCamerasOnBothSides) { this.switchCameraButtonX.setValue(-1); this.switchCameraButtonY.setValue(-1); this.switchCameraButtonWidth.setValue(0); this.switchCameraButtonHeight.setValue(0); } if (this.props.deviceOrientation !== prevProps.deviceOrientation) { this.setState({ autoFocusPointOfInterest: null }); this.cancelIndicatorAnimation.setValue(1); } if (this.props.foreground && !prevProps.foreground && this.camera) { this.camera.refreshAuthorizationStatus(); } if (this.state.stagingMode && !prevState.stagingMode) { this.cancelIndicatorAnimation.setValue(1); this.focusIndicatorOpacity.setValue(0); timing(this.stagingModeProgress, { ...stagingModeAnimationConfig, toValue: 1, }).start(); } else if (!this.state.stagingMode && prevState.stagingMode) { this.stagingModeProgress.setValue(0); } if (this.state.pendingPhotoCapture && !prevState.pendingPhotoCapture) { Animated.timing(this.sendButtonProgress, { ...sendButtonAnimationConfig, toValue: 1, }).start(); } else if ( !this.state.pendingPhotoCapture && prevState.pendingPhotoCapture ) { CameraModal.cleanUpPendingPhotoCapture(prevState.pendingPhotoCapture); this.sendButtonProgress.setValue(0); } } static async cleanUpPendingPhotoCapture(pendingPhotoCapture: PhotoCapture) { const path = pathFromURI(pendingPhotoCapture.uri); if (!path) { return; } try { await filesystem.unlink(path); } catch (e) {} } get containerStyle() { const { overlayContext } = this.props; - invariant(overlayContext, 'CameraModal should have OverlayContext'); + if (!overlayContext) { + return styles.container; + } return { ...styles.container, opacity: overlayContext.position, }; } get focusIndicatorStyle() { return { ...styles.focusIndicator, opacity: this.focusIndicatorOpacity, transform: [ { translateX: this.focusIndicatorX }, { translateY: this.focusIndicatorY }, { scale: this.focusIndicatorScale }, ], }; } renderCamera = ({ camera, status }) => { if (camera && camera._cameraHandle) { this.fetchCameraIDs(camera); } if (this.state.stagingMode) { return this.renderStagingView(); } return ( {this.renderCameraContent(status)} × ); }; renderStagingView() { let image = null; const { pendingPhotoCapture } = this.state; if (pendingPhotoCapture) { const imageSource = { uri: pendingPhotoCapture.uri }; image = ; } else { image = ; } return ( <> {image} ); } renderCameraContent(status) { if (status === 'PENDING_AUTHORIZATION') { return ; } else if (status === 'NOT_AUTHORIZED') { return ( {'don’t have permission :('} ); } let switchCameraButton = null; if (this.state.hasCamerasOnBothSides) { switchCameraButton = ( ); } let flashIcon; if (this.state.flashMode === RNCamera.Constants.FlashMode.on) { flashIcon = ; } else if (this.state.flashMode === RNCamera.Constants.FlashMode.off) { flashIcon = ; } else { flashIcon = ( <> A ); } return ( {flashIcon} {switchCameraButton} ); } render() { const statusBar = CameraModal.isActive(this.props) ? (