diff --git a/native/calendar/thread-picker-modal.react.js b/native/calendar/thread-picker-modal.react.js index 90e482b34..50896aad0 100644 --- a/native/calendar/thread-picker-modal.react.js +++ b/native/calendar/thread-picker-modal.react.js @@ -1,99 +1,99 @@ // @flow import invariant from 'invariant'; import * as React from 'react'; import { StyleSheet } from 'react-native'; import { useDispatch } from 'react-redux'; import { createLocalEntry, createLocalEntryActionType, } from 'lib/actions/entry-actions'; import { threadSearchIndex } from 'lib/selectors/nav-selectors'; import { onScreenEntryEditableThreadInfos } from 'lib/selectors/thread-selectors'; import Modal from '../components/modal.react'; import ThreadList from '../components/thread-list.react'; import { RootNavigatorContext } from '../navigation/root-navigator-context'; import type { RootNavigationProp } from '../navigation/root-navigator.react'; import type { NavigationRoute } from '../navigation/route-names'; import { useSelector } from '../redux/redux-utils'; import { waitForInteractions } from '../utils/timers'; export type ThreadPickerModalParams = { - presentedFrom: string, - dateString: string, + +presentedFrom: string, + +dateString: string, }; type Props = { +navigation: RootNavigationProp<'ThreadPickerModal'>, +route: NavigationRoute<'ThreadPickerModal'>, }; function ThreadPickerModal(props: Props): React.Node { const { navigation, route: { params: { dateString }, }, } = props; const viewerID = useSelector( state => state.currentUserInfo && state.currentUserInfo.id, ); const nextLocalID = useSelector(state => state.nextLocalID); const dispatch = useDispatch(); const rootNavigatorContext = React.useContext(RootNavigatorContext); const threadPicked = React.useCallback( (threadID: string) => { invariant( dateString && viewerID && rootNavigatorContext, 'inputs to threadPicked should be set', ); rootNavigatorContext.setKeyboardHandlingEnabled(false); dispatch({ type: createLocalEntryActionType, payload: createLocalEntry(threadID, nextLocalID, dateString, viewerID), }); }, [rootNavigatorContext, dispatch, viewerID, nextLocalID, dateString], ); React.useEffect( () => navigation.addListener('blur', async () => { await waitForInteractions(); invariant( rootNavigatorContext, 'RootNavigatorContext should be set in onScreenBlur', ); rootNavigatorContext.setKeyboardHandlingEnabled(true); }), [navigation, rootNavigatorContext], ); const index = useSelector(state => threadSearchIndex(state)); const onScreenThreadInfos = useSelector(state => onScreenEntryEditableThreadInfos(state), ); return ( ); } const styles = StyleSheet.create({ threadListItem: { paddingLeft: 10, paddingRight: 10, paddingVertical: 2, }, }); export default ThreadPickerModal; diff --git a/native/chat/settings/color-selector-modal.react.js b/native/chat/settings/color-selector-modal.react.js index 0ccd6d3d4..cb2e2491b 100644 --- a/native/chat/settings/color-selector-modal.react.js +++ b/native/chat/settings/color-selector-modal.react.js @@ -1,185 +1,185 @@ // @flow import * as React from 'react'; import { TouchableHighlight, Alert } from 'react-native'; import Icon from 'react-native-vector-icons/FontAwesome'; import { changeThreadSettingsActionTypes, changeThreadSettings, } from 'lib/actions/thread-actions'; import { type ThreadInfo, type ChangeThreadSettingsPayload, type UpdateThreadRequest, } from 'lib/types/thread-types'; import type { DispatchActionPromise } from 'lib/utils/action-utils'; import { useServerCall, useDispatchActionPromise, } from 'lib/utils/action-utils'; import ColorSelector from '../../components/color-selector.react'; import Modal from '../../components/modal.react'; import type { RootNavigationProp } from '../../navigation/root-navigator.react'; import type { NavigationRoute } from '../../navigation/route-names'; import { useSelector } from '../../redux/redux-utils'; import { type Colors, useStyles, useColors } from '../../themes/colors'; export type ColorSelectorModalParams = { - presentedFrom: string, - color: string, - threadInfo: ThreadInfo, - setColor: (color: string) => void, + +presentedFrom: string, + +color: string, + +threadInfo: ThreadInfo, + +setColor: (color: string) => void, }; type BaseProps = { +navigation: RootNavigationProp<'ColorSelectorModal'>, +route: NavigationRoute<'ColorSelectorModal'>, }; type Props = { ...BaseProps, // Redux state +colors: Colors, +styles: typeof unboundStyles, +windowWidth: number, // Redux dispatch functions +dispatchActionPromise: DispatchActionPromise, // async functions that hit server APIs +changeThreadSettings: ( request: UpdateThreadRequest, ) => Promise, }; function ColorSelectorModal(props: Props): React.Node { const { changeThreadSettings: updateThreadSettings, dispatchActionPromise, windowWidth, } = props; const { threadInfo, setColor } = props.route.params; const close = props.navigation.goBackOnce; const onErrorAcknowledged = React.useCallback(() => { setColor(threadInfo.color); }, [setColor, threadInfo.color]); const editColor = React.useCallback( async (newColor: string) => { const threadID = threadInfo.id; try { return await updateThreadSettings({ threadID, changes: { color: newColor }, }); } catch (e) { Alert.alert( 'Unknown error', 'Uhh... try again?', [{ text: 'OK', onPress: onErrorAcknowledged }], { cancelable: false }, ); throw e; } }, [onErrorAcknowledged, threadInfo.id, updateThreadSettings], ); const onColorSelected = React.useCallback( (color: string) => { const colorEditValue = color.substr(1); setColor(colorEditValue); close(); dispatchActionPromise( changeThreadSettingsActionTypes, editColor(colorEditValue), { customKeyName: `${changeThreadSettingsActionTypes.started}:color` }, ); }, [setColor, close, dispatchActionPromise, editColor], ); const { colorSelectorContainer, closeButton, closeButtonIcon } = props.styles; // Based on the assumption we are always in portrait, // and consequently width is the lowest dimensions const modalStyle = React.useMemo( () => [colorSelectorContainer, { height: 0.75 * windowWidth }], [colorSelectorContainer, windowWidth], ); const { modalIosHighlightUnderlay } = props.colors; const { color } = props.route.params; return ( ); } const unboundStyles = { closeButton: { borderRadius: 3, height: 18, position: 'absolute', right: 5, top: 5, width: 18, }, closeButtonIcon: { color: 'modalBackgroundSecondaryLabel', left: 3, position: 'absolute', }, colorSelector: { bottom: 10, left: 10, position: 'absolute', right: 10, top: 10, }, colorSelectorContainer: { backgroundColor: 'modalBackground', borderColor: 'modalForegroundBorder', borderRadius: 5, borderWidth: 2, flex: 0, marginHorizontal: 15, marginVertical: 20, }, }; const ConnectedColorSelectorModal: React.ComponentType = React.memo( function ConnectedColorSelectorModal(props: BaseProps) { const styles = useStyles(unboundStyles); const colors = useColors(); const windowWidth = useSelector(state => state.dimensions.width); const dispatchActionPromise = useDispatchActionPromise(); const callChangeThreadSettings = useServerCall(changeThreadSettings); return ( ); }, ); export default ConnectedColorSelectorModal; diff --git a/native/chat/settings/compose-subchannel-modal.react.js b/native/chat/settings/compose-subchannel-modal.react.js index 4ff1c75ae..55ee91626 100644 --- a/native/chat/settings/compose-subchannel-modal.react.js +++ b/native/chat/settings/compose-subchannel-modal.react.js @@ -1,150 +1,150 @@ // @flow import * as React from 'react'; import { Text } from 'react-native'; import IonIcon from 'react-native-vector-icons/Ionicons'; import { threadTypeDescriptions } from 'lib/shared/thread-utils'; import { type ThreadInfo, threadTypes } from 'lib/types/thread-types'; import Button from '../../components/button.react'; import Modal from '../../components/modal.react'; import SWMansionIcon from '../../components/swmansion-icon.react'; import type { RootNavigationProp } from '../../navigation/root-navigator.react'; import type { NavigationRoute } from '../../navigation/route-names'; import { ComposeSubchannelRouteName } from '../../navigation/route-names'; import { type Colors, useStyles, useColors } from '../../themes/colors'; export type ComposeSubchannelModalParams = { - presentedFrom: string, - threadInfo: ThreadInfo, + +presentedFrom: string, + +threadInfo: ThreadInfo, }; type BaseProps = { +navigation: RootNavigationProp<'ComposeSubchannelModal'>, +route: NavigationRoute<'ComposeSubchannelModal'>, }; type Props = { ...BaseProps, +colors: Colors, +styles: typeof unboundStyles, }; class ComposeSubchannelModal extends React.PureComponent { render() { return ( Chat type ); } onPressOpen = () => { const threadInfo = this.props.route.params.threadInfo; this.props.navigation.navigate<'ComposeSubchannel'>({ name: ComposeSubchannelRouteName, params: { threadType: threadTypes.COMMUNITY_OPEN_SUBTHREAD, parentThreadInfo: threadInfo, }, key: `${ComposeSubchannelRouteName}|` + `${threadInfo.id}|${threadTypes.COMMUNITY_OPEN_SUBTHREAD}`, }); }; onPressSecret = () => { const threadInfo = this.props.route.params.threadInfo; this.props.navigation.navigate<'ComposeSubchannel'>({ name: ComposeSubchannelRouteName, params: { threadType: threadTypes.COMMUNITY_SECRET_SUBTHREAD, parentThreadInfo: threadInfo, }, key: `${ComposeSubchannelRouteName}|` + `${threadInfo.id}|${threadTypes.COMMUNITY_SECRET_SUBTHREAD}`, }); }; } const unboundStyles = { forwardIcon: { color: 'modalForegroundSecondaryLabel', paddingLeft: 10, }, modal: { flex: 0, }, option: { alignItems: 'center', flexDirection: 'row', paddingHorizontal: 10, paddingVertical: 20, }, optionExplanation: { color: 'modalBackgroundLabel', flex: 1, fontSize: 14, paddingLeft: 20, textAlign: 'center', }, optionText: { color: 'modalBackgroundLabel', fontSize: 20, paddingLeft: 5, }, visibility: { color: 'modalBackgroundLabel', fontSize: 24, textAlign: 'center', }, visibilityIcon: { color: 'modalBackgroundLabel', paddingRight: 3, }, }; const ConnectedComposeSubchannelModal: React.ComponentType = React.memo( function ConnectedComposeSubchannelModal(props: BaseProps) { const styles = useStyles(unboundStyles); const colors = useColors(); return ( ); }, ); export default ConnectedComposeSubchannelModal; diff --git a/native/media/image-modal.react.js b/native/media/image-modal.react.js index 8ab9a8f20..b9132b17e 100644 --- a/native/media/image-modal.react.js +++ b/native/media/image-modal.react.js @@ -1,1280 +1,1280 @@ // @flow import Clipboard from '@react-native-community/clipboard'; 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, } from 'react-native-gesture-handler'; import Orientation from 'react-native-orientation-locker'; import Animated from 'react-native-reanimated'; import { type MediaInfo, type Dimensions } from 'lib/types/media-types'; import { useIsReportEnabled } from 'lib/utils/report-utils'; import SWMansionIcon from '../components/swmansion-icon.react'; import ConnectedStatusBar from '../connected-status-bar.react'; import { displayActionResultModal } from '../navigation/action-result-modal'; import type { AppNavigationProp } from '../navigation/app-navigator.react'; import { OverlayContext, type OverlayContextType, } from '../navigation/overlay-context'; import type { NavigationRoute } from '../navigation/route-names'; import { useSelector } from '../redux/redux-utils'; import { type DerivedDimensionsInfo, derivedDimensionsInfoSelector, } from '../selectors/dimensions-selectors'; import type { ChatMultimediaMessageInfoItem } from '../types/chat-types'; import { type VerticalBounds, type LayoutCoordinates, } from '../types/layout-types'; import type { NativeMethods } from '../types/react-native'; import { clamp, gestureJustStarted, gestureJustEnded, runTiming, } from '../utils/animation-utils'; import Multimedia from './multimedia.react'; import { intentionalSaveMedia } from './save-media'; /* 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, ]); } export type ImageModalParams = { - presentedFrom: string, - mediaInfo: MediaInfo, - initialCoordinates: LayoutCoordinates, - verticalBounds: VerticalBounds, - item: ChatMultimediaMessageInfoItem, + +presentedFrom: string, + +mediaInfo: MediaInfo, + +initialCoordinates: LayoutCoordinates, + +verticalBounds: VerticalBounds, + +item: ChatMultimediaMessageInfoItem, }; type TouchableOpacityInstance = React.AbstractComponent< React.ElementConfig, NativeMethods, >; type BaseProps = { +navigation: AppNavigationProp<'ImageModal'>, +route: NavigationRoute<'ImageModal'>, }; type Props = { ...BaseProps, // Redux state +dimensions: DerivedDimensionsInfo, // withOverlayContext +overlayContext: ?OverlayContextType, +mediaReportsEnabled: boolean, }; type State = { +closeButtonEnabled: boolean, +actionLinksEnabled: boolean, }; class ImageModal 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 = React.createRef(); panHandler = React.createRef(); singleTapHandler = React.createRef(); doubleTapHandler = React.createRef(); handlerRefs = [ this.pinchHandler, this.panHandler, this.singleTapHandler, this.doubleTapHandler, ]; beforeDoubleTapRefs; beforeSingleTapRefs; pinchEvent; panEvent; singleTapEvent; doubleTapEvent; 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, 'ImageModal 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.imageDimensions; 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 (ImageModal.isActive(this.props)) { Orientation.unlockAllOrientations(); } } componentWillUnmount() { if (ImageModal.isActive(this.props)) { Orientation.lockToPortrait(); } } componentDidUpdate(prevProps: Props) { if (this.props.dimensions !== prevProps.dimensions) { this.updateDimensions(); } const isActive = ImageModal.isActive(this.props); const wasActive = ImageModal.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 imageDimensions(): Dimensions { // Make space for the close button let { height: maxHeight, width: maxWidth } = this.frame; if (maxHeight > maxWidth) { maxHeight -= 100; } else { maxWidth -= 100; } const { dimensions } = this.props.route.params.mediaInfo; if (dimensions.height < maxHeight && dimensions.width < maxWidth) { return dimensions; } const heightRatio = maxHeight / dimensions.height; const widthRatio = maxWidth / dimensions.width; if (heightRatio < widthRatio) { return { height: maxHeight, width: dimensions.width * heightRatio, }; } else { return { width: maxWidth, height: dimensions.height * widthRatio, }; } } get imageContainerStyle() { const { height, width } = this.imageDimensions; 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, 'ImageModal 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 = ImageModal.isActive(this.props) ? { paddingTop: top, paddingBottom: bottom } : { marginTop: top, marginBottom: bottom }; return [styles.contentContainer, verticalStyle]; } render() { const { mediaInfo } = this.props.route.params; const statusBar = ImageModal.isActive(this.props) ? (