diff --git a/native/components/full-screen-view-modal.react.js b/native/components/full-screen-view-modal.react.js index 702152038..6ee0f47c6 100644 --- a/native/components/full-screen-view-modal.react.js +++ b/native/components/full-screen-view-modal.react.js @@ -1,1272 +1,1273 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { View, Text, StyleSheet, TouchableOpacity, Platform, } from 'react-native'; import { PinchGestureHandler, PanGestureHandler, TapGestureHandler, State as GestureState, type PinchGestureEvent, type PanGestureEvent, type TapGestureEvent, } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import Animated from 'react-native-reanimated'; import type { EventResult } from 'react-native-reanimated'; import { SafeAreaView } from 'react-native-safe-area-context'; import { type Dimensions } from 'lib/types/media-types.js'; import type { ReactRef } from 'lib/types/react-types.js'; import SWMansionIcon from './swmansion-icon.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 { NavigationRoute } from '../navigation/route-names.js'; import { useSelector } from '../redux/redux-utils.js'; import { type DerivedDimensionsInfo, derivedDimensionsInfoSelector, } from '../selectors/dimensions-selectors.js'; import type { NativeMethods } from '../types/react-native.js'; +import type { AnimatedStyleObj } from '../types/styles.js'; import type { UserProfileBottomSheetNavigationProp } from '../user-profile/user-profile-bottom-sheet-navigator.react.js'; import { clamp, gestureJustStarted, gestureJustEnded, runTiming, } 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, neq, greaterThan, lessThan, add, sub, multiply, divide, pow, max, min, round, abs, interpolateNode, startClock, stopClock, clockRunning, decay, } = Animated; /* eslint-enable import/no-named-as-default-member */ function scaleDelta(value: Node, gestureActive: Node): Node { const diffThisFrame = new Value(1); const prevValue = new Value(1); return cond( gestureActive, [ set(diffThisFrame, divide(value, prevValue)), set(prevValue, value), diffThisFrame, ], set(prevValue, 1), ); } function panDelta(value: Node, gestureActive: Node): Node { const diffThisFrame = new Value(0); const prevValue = new Value(0); return cond( gestureActive, [ set(diffThisFrame, sub(value, prevValue)), set(prevValue, value), diffThisFrame, ], set(prevValue, 0), ); } function runDecay( clock: Clock, velocity: Node, initialPosition: Node, startStopClock: boolean = true, ): Node { const state = { finished: new Value(0), velocity: new Value(0), position: new Value(0), time: new Value(0), }; const config = { deceleration: 0.99 }; return block([ cond(not(clockRunning(clock)), [ set(state.finished, 0), set(state.velocity, velocity), set(state.position, initialPosition), set(state.time, 0), startStopClock ? startClock(clock) : undefined, ]), decay(clock, state, config), cond(state.finished, startStopClock ? stopClock(clock) : undefined), state.position, ]); } type TouchableOpacityInstance = React.AbstractComponent< React.ElementConfig, NativeMethods, >; type BaseProps = { +navigation: | AppNavigationProp<'ImageModal'> | UserProfileBottomSheetNavigationProp<'UserProfileAvatarModal'>, +route: | NavigationRoute<'ImageModal'> | NavigationRoute<'UserProfileAvatarModal'>, +children: React.Node, +contentDimensions: Dimensions, +saveContentCallback?: () => Promise, +copyContentCallback?: () => mixed, }; type Props = { ...BaseProps, // Redux state +dimensions: DerivedDimensionsInfo, // withOverlayContext +overlayContext: ?OverlayContextType, }; type State = { +closeButtonEnabled: boolean, +actionLinksEnabled: boolean, }; class FullScreenViewModal extends React.PureComponent { state: State = { closeButtonEnabled: true, actionLinksEnabled: true, }; closeButton: ?React.ElementRef; mediaIconsContainer: ?React.ElementRef; closeButtonX = new Value(-1); closeButtonY = new Value(-1); closeButtonWidth = new Value(0); closeButtonHeight = new Value(0); closeButtonLastState = new Value(1); mediaIconsX = new Value(-1); mediaIconsY = new Value(-1); mediaIconsWidth = new Value(0); mediaIconsHeight = new Value(0); actionLinksLastState = new Value(1); centerX: Value; centerY: Value; frameWidth: Value; frameHeight: Value; imageWidth: Value; imageHeight: Value; pinchHandler: ReactRef = React.createRef(); panHandler: ReactRef = React.createRef(); singleTapHandler: ReactRef = React.createRef(); doubleTapHandler: ReactRef = React.createRef(); handlerRefs: $ReadOnlyArray< | ReactRef | ReactRef | ReactRef, > = [ this.pinchHandler, this.panHandler, this.singleTapHandler, this.doubleTapHandler, ]; beforeDoubleTapRefs: $ReadOnlyArray< | ReactRef | ReactRef | ReactRef, >; beforeSingleTapRefs: $ReadOnlyArray< | ReactRef | ReactRef | ReactRef, >; pinchEvent: EventResult; panEvent: EventResult; singleTapEvent: EventResult; doubleTapEvent: EventResult; scale: Node; x: Node; y: Node; backdropOpacity: Node; imageContainerOpacity: Node; actionLinksOpacity: Node; closeButtonOpacity: Node; constructor(props: Props) { super(props); this.updateDimensions(); const { imageWidth, imageHeight } = this; const left = sub(this.centerX, divide(imageWidth, 2)); const top = sub(this.centerY, divide(imageHeight, 2)); const { initialCoordinates } = props.route.params; const initialScale = divide(initialCoordinates.width, imageWidth); const initialTranslateX = sub( initialCoordinates.x + initialCoordinates.width / 2, add(left, divide(imageWidth, 2)), ); const initialTranslateY = sub( initialCoordinates.y + initialCoordinates.height / 2, add(top, divide(imageHeight, 2)), ); const { overlayContext } = props; invariant(overlayContext, 'FullScreenViewModal should have OverlayContext'); const navigationProgress = overlayContext.position; // The inputs we receive from PanGestureHandler const panState = new Value(-1); const panTranslationX = new Value(0); const panTranslationY = new Value(0); const panVelocityX = new Value(0); const panVelocityY = new Value(0); const panAbsoluteX = new Value(0); const panAbsoluteY = new Value(0); this.panEvent = event([ { nativeEvent: { state: panState, translationX: panTranslationX, translationY: panTranslationY, velocityX: panVelocityX, velocityY: panVelocityY, absoluteX: panAbsoluteX, absoluteY: panAbsoluteY, }, }, ]); const curPanActive = new Value(0); const panActive = block([ cond( and( gestureJustStarted(panState), this.outsideButtons( sub(panAbsoluteX, panTranslationX), sub(panAbsoluteY, panTranslationY), ), ), set(curPanActive, 1), ), cond(gestureJustEnded(panState), set(curPanActive, 0)), curPanActive, ]); const lastPanActive = new Value(0); const panJustEnded = cond(eq(lastPanActive, panActive), 0, [ set(lastPanActive, panActive), eq(panActive, 0), ]); // The inputs we receive from PinchGestureHandler const pinchState = new Value(-1); const pinchScale = new Value(1); const pinchFocalX = new Value(0); const pinchFocalY = new Value(0); this.pinchEvent = event([ { nativeEvent: { state: pinchState, scale: pinchScale, focalX: pinchFocalX, focalY: pinchFocalY, }, }, ]); const pinchActive = eq(pinchState, GestureState.ACTIVE); // The inputs we receive from single TapGestureHandler const singleTapState = new Value(-1); const singleTapX = new Value(0); const singleTapY = new Value(0); this.singleTapEvent = event([ { nativeEvent: { state: singleTapState, x: singleTapX, y: singleTapY, }, }, ]); // The inputs we receive from double TapGestureHandler const doubleTapState = new Value(-1); const doubleTapX = new Value(0); const doubleTapY = new Value(0); this.doubleTapEvent = event([ { nativeEvent: { state: doubleTapState, x: doubleTapX, y: doubleTapY, }, }, ]); // The all-important outputs const curScale = new Value(1); const curX = new Value(0); const curY = new Value(0); const curBackdropOpacity = new Value(1); const curCloseButtonOpacity = new Value(1); const curActionLinksOpacity = new Value(1); // The centered variables help us know if we need to be recentered const recenteredScale = max(curScale, 1); const horizontalPanSpace = this.horizontalPanSpace(recenteredScale); const verticalPanSpace = this.verticalPanSpace(recenteredScale); const resetXClock = new Clock(); const resetYClock = new Clock(); const zoomClock = new Clock(); const dismissingFromPan = new Value(0); const roundedCurScale = divide(round(multiply(curScale, 1000)), 1000); const gestureActive = or(pinchActive, panActive); const activeInteraction = or( gestureActive, clockRunning(zoomClock), dismissingFromPan, ); const updates = [ this.pinchUpdate( pinchActive, pinchScale, pinchFocalX, pinchFocalY, curScale, curX, curY, ), this.panUpdate(panActive, panTranslationX, panTranslationY, curX, curY), this.singleTapUpdate( singleTapState, singleTapX, singleTapY, roundedCurScale, curCloseButtonOpacity, curActionLinksOpacity, ), this.doubleTapUpdate( doubleTapState, doubleTapX, doubleTapY, roundedCurScale, zoomClock, gestureActive, curScale, curX, curY, ), this.backdropOpacityUpdate( panJustEnded, pinchActive, panVelocityX, panVelocityY, roundedCurScale, curX, curY, curBackdropOpacity, dismissingFromPan, ), this.recenter( resetXClock, resetYClock, activeInteraction, recenteredScale, horizontalPanSpace, verticalPanSpace, curScale, curX, curY, ), this.flingUpdate( resetXClock, resetYClock, activeInteraction, panJustEnded, panVelocityX, panVelocityY, horizontalPanSpace, verticalPanSpace, curX, curY, ), ]; const updatedScale = [updates, curScale]; const updatedCurX = [updates, curX]; const updatedCurY = [updates, curY]; const updatedBackdropOpacity = [updates, curBackdropOpacity]; const updatedCloseButtonOpacity = [updates, curCloseButtonOpacity]; const updatedActionLinksOpacity = [updates, curActionLinksOpacity]; const reverseNavigationProgress = sub(1, navigationProgress); this.scale = add( multiply(reverseNavigationProgress, initialScale), multiply(navigationProgress, updatedScale), ); this.x = add( multiply(reverseNavigationProgress, initialTranslateX), multiply(navigationProgress, updatedCurX), ); this.y = add( multiply(reverseNavigationProgress, initialTranslateY), multiply(navigationProgress, updatedCurY), ); this.backdropOpacity = multiply(navigationProgress, updatedBackdropOpacity); this.imageContainerOpacity = interpolateNode(navigationProgress, { inputRange: [0, 0.1], outputRange: [0, 1], extrapolate: Extrapolate.CLAMP, }); const buttonOpacity = interpolateNode(updatedBackdropOpacity, { inputRange: [0.95, 1], outputRange: [0, 1], extrapolate: Extrapolate.CLAMP, }); this.closeButtonOpacity = multiply( navigationProgress, buttonOpacity, updatedCloseButtonOpacity, ); this.actionLinksOpacity = multiply( navigationProgress, buttonOpacity, updatedActionLinksOpacity, ); this.beforeDoubleTapRefs = Platform.select({ android: [], default: [this.pinchHandler, this.panHandler], }); this.beforeSingleTapRefs = [ ...this.beforeDoubleTapRefs, this.doubleTapHandler, ]; } // How much space do we have to pan the image horizontally? horizontalPanSpace(scale: Node): Node { const apparentWidth = multiply(this.imageWidth, scale); const horizPop = divide(sub(apparentWidth, this.frameWidth), 2); return max(horizPop, 0); } // How much space do we have to pan the image vertically? verticalPanSpace(scale: Node): Node { const apparentHeight = multiply(this.imageHeight, scale); const vertPop = divide(sub(apparentHeight, this.frameHeight), 2); return max(vertPop, 0); } pinchUpdate( // Inputs pinchActive: Node, pinchScale: Node, pinchFocalX: Node, pinchFocalY: Node, // Outputs curScale: Value, curX: Value, curY: Value, ): Node { const deltaScale = scaleDelta(pinchScale, pinchActive); const deltaPinchX = multiply( sub(1, deltaScale), sub(pinchFocalX, curX, this.centerX), ); const deltaPinchY = multiply( sub(1, deltaScale), sub(pinchFocalY, curY, this.centerY), ); return cond( [deltaScale, pinchActive], [ set(curX, add(curX, deltaPinchX)), set(curY, add(curY, deltaPinchY)), set(curScale, multiply(curScale, deltaScale)), ], ); } outsideButtons(x: Node, y: Node): Node { const { closeButtonX, closeButtonY, closeButtonWidth, closeButtonHeight, closeButtonLastState, mediaIconsX, mediaIconsY, mediaIconsWidth, mediaIconsHeight, actionLinksLastState, } = this; return and( or( eq(closeButtonLastState, 0), lessThan(x, closeButtonX), greaterThan(x, add(closeButtonX, closeButtonWidth)), lessThan(y, closeButtonY), greaterThan(y, add(closeButtonY, closeButtonHeight)), ), or( eq(actionLinksLastState, 0), lessThan(x, mediaIconsX), greaterThan(x, add(mediaIconsX, mediaIconsWidth)), lessThan(y, mediaIconsY), greaterThan(y, add(mediaIconsY, mediaIconsHeight)), ), ); } panUpdate( // Inputs panActive: Node, panTranslationX: Node, panTranslationY: Node, // Outputs curX: Value, curY: Value, ): Node { const deltaX = panDelta(panTranslationX, panActive); const deltaY = panDelta(panTranslationY, panActive); return cond( [deltaX, deltaY, panActive], [set(curX, add(curX, deltaX)), set(curY, add(curY, deltaY))], ); } singleTapUpdate( // Inputs singleTapState: Node, singleTapX: Node, singleTapY: Node, roundedCurScale: Node, // Outputs curCloseButtonOpacity: Value, curActionLinksOpacity: Value, ): Node { const lastTapX = new Value(0); const lastTapY = new Value(0); const fingerJustReleased = and( gestureJustEnded(singleTapState), this.outsideButtons(lastTapX, lastTapY), ); const wasZoomed = new Value(0); const isZoomed = greaterThan(roundedCurScale, 1); const becameUnzoomed = and(wasZoomed, not(isZoomed)); const closeButtonState = cond( or( fingerJustReleased, and(becameUnzoomed, eq(this.closeButtonLastState, 0)), ), sub(1, this.closeButtonLastState), this.closeButtonLastState, ); const actionLinksState = cond( isZoomed, 0, cond( or(fingerJustReleased, becameUnzoomed), sub(1, this.actionLinksLastState), this.actionLinksLastState, ), ); const closeButtonAppearClock = new Clock(); const closeButtonDisappearClock = new Clock(); const actionLinksAppearClock = new Clock(); const actionLinksDisappearClock = new Clock(); return block([ fingerJustReleased, set( curCloseButtonOpacity, cond( eq(closeButtonState, 1), [ stopClock(closeButtonDisappearClock), runTiming(closeButtonAppearClock, curCloseButtonOpacity, 1), ], [ stopClock(closeButtonAppearClock), runTiming(closeButtonDisappearClock, curCloseButtonOpacity, 0), ], ), ), set( curActionLinksOpacity, cond( eq(actionLinksState, 1), [ stopClock(actionLinksDisappearClock), runTiming(actionLinksAppearClock, curActionLinksOpacity, 1), ], [ stopClock(actionLinksAppearClock), runTiming(actionLinksDisappearClock, curActionLinksOpacity, 0), ], ), ), set(this.actionLinksLastState, actionLinksState), set(this.closeButtonLastState, closeButtonState), set(wasZoomed, isZoomed), set(lastTapX, singleTapX), set(lastTapY, singleTapY), call([eq(curCloseButtonOpacity, 1)], this.setCloseButtonEnabled), call([eq(curActionLinksOpacity, 1)], this.setActionLinksEnabled), ]); } doubleTapUpdate( // Inputs doubleTapState: Node, doubleTapX: Node, doubleTapY: Node, roundedCurScale: Node, zoomClock: Clock, gestureActive: Node, // Outputs curScale: Value, curX: Value, curY: Value, ): Node { const zoomClockRunning = clockRunning(zoomClock); const zoomActive = and(not(gestureActive), zoomClockRunning); const targetScale = cond(greaterThan(roundedCurScale, 1), 1, 3); const tapXDiff = sub(doubleTapX, this.centerX, curX); const tapYDiff = sub(doubleTapY, this.centerY, curY); const tapXPercent = divide(tapXDiff, this.imageWidth, curScale); const tapYPercent = divide(tapYDiff, this.imageHeight, curScale); const horizPanSpace = this.horizontalPanSpace(targetScale); const vertPanSpace = this.verticalPanSpace(targetScale); const horizPanPercent = divide(horizPanSpace, this.imageWidth, targetScale); const vertPanPercent = divide(vertPanSpace, this.imageHeight, targetScale); const tapXPercentClamped = clamp( tapXPercent, multiply(-1, horizPanPercent), horizPanPercent, ); const tapYPercentClamped = clamp( tapYPercent, multiply(-1, vertPanPercent), vertPanPercent, ); const targetX = multiply(tapXPercentClamped, this.imageWidth, targetScale); const targetY = multiply(tapYPercentClamped, this.imageHeight, targetScale); const targetRelativeScale = divide(targetScale, curScale); const targetRelativeX = multiply(-1, add(targetX, curX)); const targetRelativeY = multiply(-1, add(targetY, curY)); const zoomScale = runTiming(zoomClock, 1, targetRelativeScale); const zoomX = runTiming(zoomClock, 0, targetRelativeX, false); const zoomY = runTiming(zoomClock, 0, targetRelativeY, false); const deltaScale = scaleDelta(zoomScale, zoomActive); const deltaX = panDelta(zoomX, zoomActive); const deltaY = panDelta(zoomY, zoomActive); const fingerJustReleased = and( gestureJustEnded(doubleTapState), this.outsideButtons(doubleTapX, doubleTapY), ); return cond( [fingerJustReleased, deltaX, deltaY, deltaScale, gestureActive], stopClock(zoomClock), cond(or(zoomClockRunning, fingerJustReleased), [ zoomX, zoomY, zoomScale, set(curX, add(curX, deltaX)), set(curY, add(curY, deltaY)), set(curScale, multiply(curScale, deltaScale)), ]), ); } backdropOpacityUpdate( // Inputs panJustEnded: Node, pinchActive: Node, panVelocityX: Node, panVelocityY: Node, roundedCurScale: Node, // Outputs curX: Value, curY: Value, curBackdropOpacity: Value, dismissingFromPan: Value, ): Node { const progressiveOpacity = max( min( sub(1, abs(divide(curX, this.frameWidth))), sub(1, abs(divide(curY, this.frameHeight))), ), 0, ); const resetClock = new Clock(); const velocity = pow(add(pow(panVelocityX, 2), pow(panVelocityY, 2)), 0.5); const shouldGoBack = and( panJustEnded, or(greaterThan(velocity, 50), greaterThan(0.7, progressiveOpacity)), ); const decayClock = new Clock(); const decayItems = [ set(curX, runDecay(decayClock, panVelocityX, curX, false)), set(curY, runDecay(decayClock, panVelocityY, curY)), ]; return cond( [panJustEnded, dismissingFromPan], decayItems, cond( or(pinchActive, greaterThan(roundedCurScale, 1)), set(curBackdropOpacity, runTiming(resetClock, curBackdropOpacity, 1)), [ stopClock(resetClock), set(curBackdropOpacity, progressiveOpacity), set(dismissingFromPan, shouldGoBack), cond(shouldGoBack, [decayItems, call([], this.close)]), ], ), ); } recenter( // Inputs resetXClock: Clock, resetYClock: Clock, activeInteraction: Node, recenteredScale: Node, horizontalPanSpace: Node, verticalPanSpace: Node, // Outputs curScale: Value, curX: Value, curY: Value, ): Node { const resetScaleClock = new Clock(); const recenteredX = clamp( curX, multiply(-1, horizontalPanSpace), horizontalPanSpace, ); const recenteredY = clamp( curY, multiply(-1, verticalPanSpace), verticalPanSpace, ); return cond( activeInteraction, [ stopClock(resetScaleClock), stopClock(resetXClock), stopClock(resetYClock), ], [ cond( or(clockRunning(resetScaleClock), neq(recenteredScale, curScale)), set(curScale, runTiming(resetScaleClock, curScale, recenteredScale)), ), cond( or(clockRunning(resetXClock), neq(recenteredX, curX)), set(curX, runTiming(resetXClock, curX, recenteredX)), ), cond( or(clockRunning(resetYClock), neq(recenteredY, curY)), set(curY, runTiming(resetYClock, curY, recenteredY)), ), ], ); } flingUpdate( // Inputs resetXClock: Clock, resetYClock: Clock, activeInteraction: Node, panJustEnded: Node, panVelocityX: Node, panVelocityY: Node, horizontalPanSpace: Node, verticalPanSpace: Node, // Outputs curX: Value, curY: Value, ): Node { const flingXClock = new Clock(); const flingYClock = new Clock(); const decayX = runDecay(flingXClock, panVelocityX, curX); const recenteredX = clamp( decayX, multiply(-1, horizontalPanSpace), horizontalPanSpace, ); const decayY = runDecay(flingYClock, panVelocityY, curY); const recenteredY = clamp( decayY, multiply(-1, verticalPanSpace), verticalPanSpace, ); return cond( activeInteraction, [stopClock(flingXClock), stopClock(flingYClock)], [ cond( clockRunning(resetXClock), stopClock(flingXClock), cond(or(panJustEnded, clockRunning(flingXClock)), [ set(curX, recenteredX), cond(neq(decayX, recenteredX), stopClock(flingXClock)), ]), ), cond( clockRunning(resetYClock), stopClock(flingYClock), cond(or(panJustEnded, clockRunning(flingYClock)), [ set(curY, recenteredY), cond(neq(decayY, recenteredY), stopClock(flingYClock)), ]), ), ], ); } updateDimensions() { const { width: frameWidth, height: frameHeight } = this.frame; const { topInset } = this.props.dimensions; if (this.frameWidth) { this.frameWidth.setValue(frameWidth); } else { this.frameWidth = new Value(frameWidth); } if (this.frameHeight) { this.frameHeight.setValue(frameHeight); } else { this.frameHeight = new Value(frameHeight); } const centerX = frameWidth / 2; const centerY = frameHeight / 2 + topInset; if (this.centerX) { this.centerX.setValue(centerX); } else { this.centerX = new Value(centerX); } if (this.centerY) { this.centerY.setValue(centerY); } else { this.centerY = new Value(centerY); } const { width, height } = this.props.contentDimensions; if (this.imageWidth) { this.imageWidth.setValue(width); } else { this.imageWidth = new Value(width); } if (this.imageHeight) { this.imageHeight.setValue(height); } else { this.imageHeight = new Value(height); } } componentDidMount() { if (FullScreenViewModal.isActive(this.props)) { Orientation.unlockAllOrientations(); } } componentWillUnmount() { if (FullScreenViewModal.isActive(this.props)) { Orientation.lockToPortrait(); } } componentDidUpdate(prevProps: Props) { if (this.props.dimensions !== prevProps.dimensions) { this.updateDimensions(); } const isActive = FullScreenViewModal.isActive(this.props); const wasActive = FullScreenViewModal.isActive(prevProps); if (isActive && !wasActive) { Orientation.unlockAllOrientations(); } else if (!isActive && wasActive) { Orientation.lockToPortrait(); } } get frame(): Dimensions { const { width, safeAreaHeight } = this.props.dimensions; return { width, height: safeAreaHeight }; } - get contentViewContainerStyle() { + get contentViewContainerStyle(): AnimatedStyleObj { const { height, width } = this.props.contentDimensions; const { height: frameHeight, width: frameWidth } = this.frame; const top = (frameHeight - height) / 2 + this.props.dimensions.topInset; const left = (frameWidth - width) / 2; const { verticalBounds } = this.props.route.params; return { height, width, marginTop: top - verticalBounds.y, marginLeft: left, opacity: this.imageContainerOpacity, transform: [ { translateX: this.x }, { translateY: this.y }, { scale: this.scale }, ], }; } static isActive(props) { const { overlayContext } = props; invariant(overlayContext, 'FullScreenViewModal should have OverlayContext'); return !overlayContext.isDismissing; } get contentContainerStyle() { const { verticalBounds } = this.props.route.params; const fullScreenHeight = this.props.dimensions.height; const top = verticalBounds.y; const bottom = fullScreenHeight - verticalBounds.y - verticalBounds.height; // margin will clip, but padding won't const verticalStyle = FullScreenViewModal.isActive(this.props) ? { paddingTop: top, paddingBottom: bottom } : { marginTop: top, marginBottom: bottom }; return [styles.contentContainer, verticalStyle]; } render() { const { children, saveContentCallback, copyContentCallback } = this.props; const statusBar = FullScreenViewModal.isActive(this.props) ? (