diff --git a/package.json b/package.json index 21e632aa45..3e65f7bb85 100644 --- a/package.json +++ b/package.json @@ -126,6 +126,7 @@ "expo-media-library": "~15.9.1", "expo-navigation-bar": "~2.8.1", "expo-notifications": "~0.27.6", + "expo-screen-orientation": "^6.4.1", "expo-sharing": "^11.10.0", "expo-splash-screen": "~0.26.4", "expo-status-bar": "~1.11.1", diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx index 414f98a611..d9c5ef40b3 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx @@ -1,30 +1,29 @@ -import React, {useState} from 'react' - -import {ActivityIndicator, Dimensions, StyleSheet} from 'react-native' -import {Image} from 'expo-image' +import React, {useMemo, useState} from 'react' +import {ActivityIndicator, StyleSheet, useWindowDimensions} from 'react-native' +import {Gesture, GestureDetector} from 'react-native-gesture-handler' import Animated, { runOnJS, + useAnimatedReaction, useAnimatedRef, useAnimatedStyle, - useAnimatedReaction, useSharedValue, withDecay, withSpring, } from 'react-native-reanimated' -import {GestureDetector, Gesture} from 'react-native-gesture-handler' +import {Image} from 'expo-image' + +import type {Dimensions as ImageDimensions, ImageSource} from '../../@types' import useImageDimensions from '../../hooks/useImageDimensions' import { - createTransform, - readTransform, applyRounding, + createTransform, prependPan, prependPinch, prependTransform, + readTransform, TransformMatrix, } from '../../transforms' -import type {ImageSource, Dimensions as ImageDimensions} from '../../@types' -const SCREEN = Dimensions.get('window') const MIN_DOUBLE_TAP_SCALE = 2 const MAX_ORIGINAL_IMAGE_ZOOM = 2 @@ -56,6 +55,7 @@ const ImageItem = ({ const pinchTranslation = useSharedValue({x: 0, y: 0}) const dismissSwipeTranslateY = useSharedValue(0) const containerRef = useAnimatedRef() + const screen = useWindowDimensions() // Keep track of when we're entering or leaving scaled rendering. // Note: DO NOT move any logic reading animated values outside this function. @@ -96,7 +96,7 @@ const ImageItem = ({ const dismissDistance = dismissSwipeTranslateY.value const dismissProgress = Math.min( - Math.abs(dismissDistance) / (SCREEN.height / 2), + Math.abs(dismissDistance) / (screen.height / 2), 1, ) return { @@ -120,16 +120,21 @@ const ImageItem = ({ } const [nextTranslateX, nextTranslateY, nextScale] = readTransform(candidateTransform) - const scaledDimensions = getScaledDimensions(imageDimensions, nextScale) + const scaledDimensions = getScaledDimensions( + imageDimensions, + nextScale, + screen.width, + screen.height, + ) const clampedTranslateX = clampTranslation( nextTranslateX, scaledDimensions.width, - SCREEN.width, + screen.width, ) const clampedTranslateY = clampTranslation( nextTranslateY, scaledDimensions.height, - SCREEN.height, + screen.height, ) const dx = clampedTranslateX - nextTranslateX const dy = clampedTranslateY - nextTranslateY @@ -139,8 +144,8 @@ const ImageItem = ({ const pinch = Gesture.Pinch() .onStart(e => { pinchOrigin.value = { - x: e.focalX - SCREEN.width / 2, - y: e.focalY - SCREEN.height / 2, + x: e.focalX - screen.width / 2, + y: e.focalY - screen.height / 2, } }) .onChange(e => { @@ -151,7 +156,7 @@ const ImageItem = ({ // Also, like in stock Android apps, don't let the user zoom out further than 1:1. const [, , committedScale] = readTransform(committedTransform.value) const maxCommittedScale = - (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + (imageDimensions.width / screen.width) * MAX_ORIGINAL_IMAGE_ZOOM const minPinchScale = 1 / committedScale const maxPinchScale = maxCommittedScale / committedScale const nextPinchScale = Math.min( @@ -250,7 +255,7 @@ const ImageItem = ({ // Try to zoom in so that we get rid of the black bars (whatever the orientation was). const imageAspect = imageDimensions.width / imageDimensions.height - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screen.width / screen.height const candidateScale = Math.max( imageAspect / screenAspect, screenAspect / imageAspect, @@ -258,15 +263,15 @@ const ImageItem = ({ ) // But don't zoom in so close that the picture gets blurry. const maxScale = - (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + (imageDimensions.width / screen.width) * MAX_ORIGINAL_IMAGE_ZOOM const scale = Math.min(candidateScale, maxScale) // Calculate where we would be if the user pinched into the double tapped point. // We won't use this transform directly because it may go out of bounds. const candidateTransform = createTransform() const origin = { - x: e.absoluteX - SCREEN.width / 2, - y: e.absoluteY - SCREEN.height / 2, + x: e.absoluteX - screen.width / 2, + y: e.absoluteY - screen.height / 2, } prependPinch(candidateTransform, scale, origin, {x: 0, y: 0}) @@ -308,8 +313,14 @@ const ImageItem = ({ ) const isLoading = !isLoaded || !imageDimensions + + const screenSize = useMemo( + () => ({width: screen.width, height: screen.height}), + [screen.width, screen.height], + ) + return ( - + {isLoading && ( )} @@ -330,8 +341,6 @@ const ImageItem = ({ const styles = StyleSheet.create({ container: { - width: SCREEN.width, - height: SCREEN.height, overflow: 'hidden', }, image: { @@ -349,20 +358,22 @@ const styles = StyleSheet.create({ function getScaledDimensions( imageDimensions: ImageDimensions, scale: number, + screenWidth: number, + screenHeight: number, ): ImageDimensions { 'worklet' const imageAspect = imageDimensions.width / imageDimensions.height - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screenWidth / screenHeight const isLandscape = imageAspect > screenAspect if (isLandscape) { return { - width: scale * SCREEN.width, - height: (scale * SCREEN.width) / imageAspect, + width: scale * screenWidth, + height: (scale * screenWidth) / imageAspect, } } else { return { - width: scale * SCREEN.height * imageAspect, - height: scale * SCREEN.height, + width: scale * screenHeight * imageAspect, + height: scale * screenHeight, } } } diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx index 383490f4f1..15611a6a34 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx @@ -6,10 +6,9 @@ * */ -import React, {useState} from 'react' - -import {Dimensions, StyleSheet} from 'react-native' -import {Image} from 'expo-image' +import React, {useMemo, useState} from 'react' +import {useWindowDimensions} from 'react-native' +import {Gesture, GestureDetector} from 'react-native-gesture-handler' import Animated, { interpolate, runOnJS, @@ -17,17 +16,15 @@ import Animated, { useAnimatedStyle, useSharedValue, } from 'react-native-reanimated' +import {Image} from 'expo-image' + import {useAnimatedScrollHandler} from '#/lib/hooks/useAnimatedScrollHandler_FIXED' -import {Gesture, GestureDetector} from 'react-native-gesture-handler' - +import {Dimensions as ImageDimensions, ImageSource} from '../../@types' import useImageDimensions from '../../hooks/useImageDimensions' - -import {ImageSource, Dimensions as ImageDimensions} from '../../@types' import {ImageLoading} from './ImageLoading' const SWIPE_CLOSE_OFFSET = 75 const SWIPE_CLOSE_VELOCITY = 1 -const SCREEN = Dimensions.get('screen') const MAX_ORIGINAL_IMAGE_ZOOM = 2 const MIN_DOUBLE_TAP_SCALE = 2 @@ -53,9 +50,10 @@ const ImageItem = ({ const translationY = useSharedValue(0) const [loaded, setLoaded] = useState(false) const [scaled, setScaled] = useState(false) + const screen = useWindowDimensions() const imageDimensions = useImageDimensions(imageSrc) const maxZoomScale = imageDimensions - ? (imageDimensions.width / SCREEN.width) * MAX_ORIGINAL_IMAGE_ZOOM + ? (imageDimensions.width / screen.width) * MAX_ORIGINAL_IMAGE_ZOOM : 1 const animatedStyle = useAnimatedStyle(() => { @@ -98,8 +96,8 @@ const ImageItem = ({ let nextZoomRect = { x: 0, y: 0, - width: SCREEN.width, - height: SCREEN.height, + width: screen.width, + height: screen.height, } const willZoom = !scaled @@ -108,6 +106,8 @@ const ImageItem = ({ imageDimensions, absoluteX, absoluteY, + screen.width, + screen.height, ) } @@ -131,23 +131,31 @@ const ImageItem = ({ const composedGesture = Gesture.Exclusive(doubleTap, singleTap) + const screenSize = useMemo( + () => ({ + width: screen.width, + height: screen.height, + }), + [screen.width, screen.height], + ) + return ( {(!loaded || !imageDimensions) && } setLoaded(true)} @@ -158,24 +166,12 @@ const ImageItem = ({ ) } -const styles = StyleSheet.create({ - imageScrollContainer: { - height: SCREEN.height, - }, - listItem: { - width: SCREEN.width, - height: SCREEN.height, - }, - image: { - width: SCREEN.width, - height: SCREEN.height, - }, -}) - const getZoomRectAfterDoubleTap = ( imageDimensions: ImageDimensions | null, touchX: number, touchY: number, + screenWidth: number, + screenHeight: number, ): { x: number y: number @@ -186,15 +182,15 @@ const getZoomRectAfterDoubleTap = ( return { x: 0, y: 0, - width: SCREEN.width, - height: SCREEN.height, + width: screenWidth, + height: screenHeight, } } // First, let's figure out how much we want to zoom in. // We want to try to zoom in at least close enough to get rid of black bars. const imageAspect = imageDimensions.width / imageDimensions.height - const screenAspect = SCREEN.width / SCREEN.height + const screenAspect = screenWidth / screenHeight const zoom = Math.max( imageAspect / screenAspect, screenAspect / imageAspect, @@ -205,25 +201,25 @@ const getZoomRectAfterDoubleTap = ( // Next, we'll be calculating the rectangle to "zoom into" in screen coordinates. // We already know the zoom level, so this gives us the rectangle size. - let rectWidth = SCREEN.width / zoom - let rectHeight = SCREEN.height / zoom + let rectWidth = screenWidth / zoom + let rectHeight = screenHeight / zoom // Before we settle on the zoomed rect, figure out the safe area it has to be inside. // We don't want to introduce new black bars or make existing black bars unbalanced. let minX = 0 let minY = 0 - let maxX = SCREEN.width - rectWidth - let maxY = SCREEN.height - rectHeight + let maxX = screenWidth - rectWidth + let maxY = screenHeight - rectHeight if (imageAspect >= screenAspect) { // The image has horizontal black bars. Exclude them from the safe area. - const renderedHeight = SCREEN.width / imageAspect - const horizontalBarHeight = (SCREEN.height - renderedHeight) / 2 + const renderedHeight = screenWidth / imageAspect + const horizontalBarHeight = (screenHeight - renderedHeight) / 2 minY += horizontalBarHeight maxY -= horizontalBarHeight } else { // The image has vertical black bars. Exclude them from the safe area. - const renderedWidth = SCREEN.height * imageAspect - const verticalBarWidth = (SCREEN.width - renderedWidth) / 2 + const renderedWidth = screenHeight * imageAspect + const verticalBarWidth = (screenWidth - renderedWidth) / 2 minX += verticalBarWidth maxX -= verticalBarWidth } @@ -238,7 +234,7 @@ const getZoomRectAfterDoubleTap = ( rectX = Math.max(rectX, minX) } else { // Keep the rect centered on the screen so that black bars are balanced. - rectX = SCREEN.width / 2 - rectWidth / 2 + rectX = screenWidth / 2 - rectWidth / 2 } let rectY if (maxY >= minY) { @@ -249,7 +245,7 @@ const getZoomRectAfterDoubleTap = ( rectY = Math.max(rectY, minY) } else { // Keep the rect centered on the screen so that black bars are balanced. - rectY = SCREEN.height / 2 - rectHeight / 2 + rectY = screenHeight / 2 - rectHeight / 2 } return { diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx index 08b99bf9ee..4cb7903efa 100644 --- a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx +++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.tsx @@ -2,6 +2,7 @@ import React from 'react' import {View} from 'react-native' + import {ImageSource} from '../../@types' type Props = { diff --git a/src/view/com/lightbox/ImageViewing/index.tsx b/src/view/com/lightbox/ImageViewing/index.tsx index ff8fdb86d3..48289183cc 100644 --- a/src/view/com/lightbox/ImageViewing/index.tsx +++ b/src/view/com/lightbox/ImageViewing/index.tsx @@ -9,15 +9,14 @@ // https://github.com/jobtoday/react-native-image-viewing import React, {ComponentType, useCallback, useMemo, useState} from 'react' -import {StyleSheet, View, Platform} from 'react-native' - -import ImageItem from './components/ImageItem/ImageItem' -import ImageDefaultHeader from './components/ImageDefaultHeader' - -import {ImageSource} from './@types' +import {Platform, StyleSheet, View} from 'react-native' +import PagerView from 'react-native-pager-view' import Animated, {useAnimatedStyle, withSpring} from 'react-native-reanimated' import {Edge, SafeAreaView} from 'react-native-safe-area-context' -import PagerView from 'react-native-pager-view' + +import {ImageSource} from './@types' +import ImageDefaultHeader from './components/ImageDefaultHeader' +import ImageItem from './components/ImageItem/ImageItem' type Props = { images: ImageSource[] @@ -88,11 +87,11 @@ function ImageViewing({ return ( - + {typeof HeaderComponent !== 'undefined' ? ( React.createElement(HeaderComponent, { diff --git a/src/view/com/lightbox/Lightbox.tsx b/src/view/com/lightbox/Lightbox.tsx index 5bab643ca8..16b49c90b2 100644 --- a/src/view/com/lightbox/Lightbox.tsx +++ b/src/view/com/lightbox/Lightbox.tsx @@ -1,22 +1,24 @@ -import React from 'react' +import React, {useEffect} from 'react' import {LayoutAnimation, StyleSheet, View} from 'react-native' -import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' -import ImageView from './ImageViewing' -import {shareImageModal, saveImageToMediaLibrary} from 'lib/media/manip' -import * as Toast from '../util/Toast' -import {Text} from '../util/text/Text' -import {s, colors} from 'lib/styles' -import {Button} from '../util/forms/Button' -import {isIOS} from 'platform/detection' import * as MediaLibrary from 'expo-media-library' +import * as ScreenOrientation from 'expo-screen-orientation' +import {FontAwesomeIcon} from '@fortawesome/react-native-fontawesome' +import {msg, Trans} from '@lingui/macro' +import {useLingui} from '@lingui/react' + import { + ImagesLightbox, + ProfileImageLightbox, useLightbox, useLightboxControls, - ProfileImageLightbox, - ImagesLightbox, } from '#/state/lightbox' -import {Trans, msg} from '@lingui/macro' -import {useLingui} from '@lingui/react' +import {saveImageToMediaLibrary, shareImageModal} from 'lib/media/manip' +import {colors, s} from 'lib/styles' +import {isIOS} from 'platform/detection' +import {Button} from '../util/forms/Button' +import {Text} from '../util/text/Text' +import * as Toast from '../util/Toast' +import ImageView from './ImageViewing' export function Lightbox() { const {activeLightbox} = useLightbox() @@ -25,6 +27,19 @@ export function Lightbox() { closeLightbox() }, [closeLightbox]) + const isOpen = !!activeLightbox + + useEffect(() => { + if (isOpen) { + ScreenOrientation.unlockAsync() + } else { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP) + } + return () => { + ScreenOrientation.lockAsync(ScreenOrientation.OrientationLock.PORTRAIT_UP) + } + }, [isOpen]) + if (!activeLightbox) { return null } else if (activeLightbox.name === 'profile-image') { diff --git a/yarn.lock b/yarn.lock index d048bdf7fe..2c57fd4798 100644 --- a/yarn.lock +++ b/yarn.lock @@ -11994,6 +11994,11 @@ expo-pwa@0.0.127: commander "2.20.0" update-check "1.5.3" +expo-screen-orientation@^6.4.1: + version "6.4.1" + resolved "https://registry.yarnpkg.com/expo-screen-orientation/-/expo-screen-orientation-6.4.1.tgz#1c4428a058921e48b5caa45367e626b8bec10e24" + integrity sha512-VM0C9ORNL1aT6Dr2OUeryzV519n0FjtXI2m+HlijOMi1QT2bPg4tBkCd7HLgywU4dZ1Esa46ewUudmk+fOqmMQ== + expo-sharing@^11.10.0: version "11.10.0" resolved "https://registry.yarnpkg.com/expo-sharing/-/expo-sharing-11.10.0.tgz#0e85197ee4d2634b00fe201e571fbdc64cf83eef"