about summary refs log tree commit diff
path: root/src/view/com/lightbox/ImageViewing/components
diff options
context:
space:
mode:
Diffstat (limited to 'src/view/com/lightbox/ImageViewing/components')
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx52
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx152
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx152
-rw-r--r--src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx37
4 files changed, 393 insertions, 0 deletions
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
new file mode 100644
index 000000000..6880008e4
--- /dev/null
+++ b/src/view/com/lightbox/ImageViewing/components/ImageDefaultHeader.tsx
@@ -0,0 +1,52 @@
+/**
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import React from 'react'
+import {SafeAreaView, Text, TouchableOpacity, StyleSheet} from 'react-native'
+
+type Props = {
+  onRequestClose: () => void
+}
+
+const HIT_SLOP = {top: 16, left: 16, bottom: 16, right: 16}
+
+const ImageDefaultHeader = ({onRequestClose}: Props) => (
+  <SafeAreaView style={styles.root}>
+    <TouchableOpacity
+      style={styles.closeButton}
+      onPress={onRequestClose}
+      hitSlop={HIT_SLOP}>
+      <Text style={styles.closeText}>✕</Text>
+    </TouchableOpacity>
+  </SafeAreaView>
+)
+
+const styles = StyleSheet.create({
+  root: {
+    alignItems: 'flex-end',
+  },
+  closeButton: {
+    marginRight: 8,
+    marginTop: 8,
+    width: 44,
+    height: 44,
+    alignItems: 'center',
+    justifyContent: 'center',
+    borderRadius: 22,
+    backgroundColor: '#00000077',
+  },
+  closeText: {
+    lineHeight: 22,
+    fontSize: 19,
+    textAlign: 'center',
+    color: '#FFF',
+    includeFontPadding: false,
+  },
+})
+
+export default ImageDefaultHeader
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
new file mode 100644
index 000000000..01a53ff6f
--- /dev/null
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.android.tsx
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import React, {useCallback, useRef, useState} from 'react'
+
+import {
+  Animated,
+  ScrollView,
+  Dimensions,
+  StyleSheet,
+  NativeScrollEvent,
+  NativeSyntheticEvent,
+  NativeMethodsMixin,
+} from 'react-native'
+
+import useImageDimensions from '../../hooks/useImageDimensions'
+import usePanResponder from '../../hooks/usePanResponder'
+
+import {getImageStyles, getImageTransform} from '../../utils'
+import {ImageSource} from '../../@types'
+import {ImageLoading} from './ImageLoading'
+
+const SWIPE_CLOSE_OFFSET = 75
+const SWIPE_CLOSE_VELOCITY = 1.75
+const SCREEN = Dimensions.get('window')
+const SCREEN_WIDTH = SCREEN.width
+const SCREEN_HEIGHT = SCREEN.height
+
+type Props = {
+  imageSrc: ImageSource
+  onRequestClose: () => void
+  onZoom: (isZoomed: boolean) => void
+  onLongPress: (image: ImageSource) => void
+  delayLongPress: number
+  swipeToCloseEnabled?: boolean
+  doubleTapToZoomEnabled?: boolean
+}
+
+const ImageItem = ({
+  imageSrc,
+  onZoom,
+  onRequestClose,
+  onLongPress,
+  delayLongPress,
+  swipeToCloseEnabled = true,
+  doubleTapToZoomEnabled = true,
+}: Props) => {
+  const imageContainer = useRef<ScrollView & NativeMethodsMixin>(null)
+  const imageDimensions = useImageDimensions(imageSrc)
+  const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
+  const scrollValueY = new Animated.Value(0)
+  const [isLoaded, setLoadEnd] = useState(false)
+
+  const onLoaded = useCallback(() => setLoadEnd(true), [])
+  const onZoomPerformed = useCallback(
+    (isZoomed: boolean) => {
+      onZoom(isZoomed)
+      if (imageContainer?.current) {
+        imageContainer.current.setNativeProps({
+          scrollEnabled: !isZoomed,
+        })
+      }
+    },
+    [onZoom],
+  )
+
+  const onLongPressHandler = useCallback(() => {
+    onLongPress(imageSrc)
+  }, [imageSrc, onLongPress])
+
+  const [panHandlers, scaleValue, translateValue] = usePanResponder({
+    initialScale: scale || 1,
+    initialTranslate: translate || {x: 0, y: 0},
+    onZoom: onZoomPerformed,
+    doubleTapToZoomEnabled,
+    onLongPress: onLongPressHandler,
+    delayLongPress,
+  })
+
+  const imagesStyles = getImageStyles(
+    imageDimensions,
+    translateValue,
+    scaleValue,
+  )
+  const imageOpacity = scrollValueY.interpolate({
+    inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
+    outputRange: [0.7, 1, 0.7],
+  })
+  const imageStylesWithOpacity = {...imagesStyles, opacity: imageOpacity}
+
+  const onScrollEndDrag = ({
+    nativeEvent,
+  }: NativeSyntheticEvent<NativeScrollEvent>) => {
+    const velocityY = nativeEvent?.velocity?.y ?? 0
+    const offsetY = nativeEvent?.contentOffset?.y ?? 0
+
+    if (
+      (Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY &&
+        offsetY > SWIPE_CLOSE_OFFSET) ||
+      offsetY > SCREEN_HEIGHT / 2
+    ) {
+      onRequestClose()
+    }
+  }
+
+  const onScroll = ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
+    const offsetY = nativeEvent?.contentOffset?.y ?? 0
+
+    scrollValueY.setValue(offsetY)
+  }
+
+  return (
+    <ScrollView
+      ref={imageContainer}
+      style={styles.listItem}
+      pagingEnabled
+      nestedScrollEnabled
+      showsHorizontalScrollIndicator={false}
+      showsVerticalScrollIndicator={false}
+      contentContainerStyle={styles.imageScrollContainer}
+      scrollEnabled={swipeToCloseEnabled}
+      {...(swipeToCloseEnabled && {
+        onScroll,
+        onScrollEndDrag,
+      })}>
+      <Animated.Image
+        {...panHandlers}
+        source={imageSrc}
+        style={imageStylesWithOpacity}
+        onLoad={onLoaded}
+      />
+      {(!isLoaded || !imageDimensions) && <ImageLoading />}
+    </ScrollView>
+  )
+}
+
+const styles = StyleSheet.create({
+  listItem: {
+    width: SCREEN_WIDTH,
+    height: SCREEN_HEIGHT,
+  },
+  imageScrollContainer: {
+    height: SCREEN_HEIGHT * 2,
+  },
+})
+
+export default React.memo(ImageItem)
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
new file mode 100644
index 000000000..12d37e283
--- /dev/null
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageItem.ios.tsx
@@ -0,0 +1,152 @@
+/**
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import React, {useCallback, useRef, useState} from 'react'
+
+import {
+  Animated,
+  Dimensions,
+  ScrollView,
+  StyleSheet,
+  View,
+  NativeScrollEvent,
+  NativeSyntheticEvent,
+  TouchableWithoutFeedback,
+} from 'react-native'
+
+import useDoubleTapToZoom from '../../hooks/useDoubleTapToZoom'
+import useImageDimensions from '../../hooks/useImageDimensions'
+
+import {getImageStyles, getImageTransform} from '../../utils'
+import {ImageSource} from '../../@types'
+import {ImageLoading} from './ImageLoading'
+
+const SWIPE_CLOSE_OFFSET = 75
+const SWIPE_CLOSE_VELOCITY = 1
+const SCREEN = Dimensions.get('screen')
+const SCREEN_WIDTH = SCREEN.width
+const SCREEN_HEIGHT = SCREEN.height
+
+type Props = {
+  imageSrc: ImageSource
+  onRequestClose: () => void
+  onZoom: (scaled: boolean) => void
+  onLongPress: (image: ImageSource) => void
+  delayLongPress: number
+  swipeToCloseEnabled?: boolean
+  doubleTapToZoomEnabled?: boolean
+}
+
+const ImageItem = ({
+  imageSrc,
+  onZoom,
+  onRequestClose,
+  onLongPress,
+  delayLongPress,
+  swipeToCloseEnabled = true,
+  doubleTapToZoomEnabled = true,
+}: Props) => {
+  const scrollViewRef = useRef<ScrollView>(null)
+  const [loaded, setLoaded] = useState(false)
+  const [scaled, setScaled] = useState(false)
+  const imageDimensions = useImageDimensions(imageSrc)
+  const handleDoubleTap = useDoubleTapToZoom(scrollViewRef, scaled, SCREEN)
+
+  const [translate, scale] = getImageTransform(imageDimensions, SCREEN)
+  const scrollValueY = new Animated.Value(0)
+  const scaleValue = new Animated.Value(scale || 1)
+  const translateValue = new Animated.ValueXY(translate)
+  const maxScale = scale && scale > 0 ? Math.max(1 / scale, 1) : 1
+
+  const imageOpacity = scrollValueY.interpolate({
+    inputRange: [-SWIPE_CLOSE_OFFSET, 0, SWIPE_CLOSE_OFFSET],
+    outputRange: [0.5, 1, 0.5],
+  })
+  const imagesStyles = getImageStyles(
+    imageDimensions,
+    translateValue,
+    scaleValue,
+  )
+  const imageStylesWithOpacity = {...imagesStyles, opacity: imageOpacity}
+
+  const onScrollEndDrag = useCallback(
+    ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
+      const velocityY = nativeEvent?.velocity?.y ?? 0
+      const currentScaled = nativeEvent?.zoomScale > 1
+
+      onZoom(currentScaled)
+      setScaled(currentScaled)
+
+      if (
+        !currentScaled &&
+        swipeToCloseEnabled &&
+        Math.abs(velocityY) > SWIPE_CLOSE_VELOCITY
+      ) {
+        onRequestClose()
+      }
+    },
+    [onRequestClose, onZoom, swipeToCloseEnabled],
+  )
+
+  const onScroll = ({nativeEvent}: NativeSyntheticEvent<NativeScrollEvent>) => {
+    const offsetY = nativeEvent?.contentOffset?.y ?? 0
+
+    if (nativeEvent?.zoomScale > 1) {
+      return
+    }
+
+    scrollValueY.setValue(offsetY)
+  }
+
+  const onLongPressHandler = useCallback(() => {
+    onLongPress(imageSrc)
+  }, [imageSrc, onLongPress])
+
+  return (
+    <View>
+      <ScrollView
+        ref={scrollViewRef}
+        style={styles.listItem}
+        pinchGestureEnabled
+        showsHorizontalScrollIndicator={false}
+        showsVerticalScrollIndicator={false}
+        maximumZoomScale={maxScale}
+        contentContainerStyle={styles.imageScrollContainer}
+        scrollEnabled={swipeToCloseEnabled}
+        onScrollEndDrag={onScrollEndDrag}
+        scrollEventThrottle={1}
+        {...(swipeToCloseEnabled && {
+          onScroll,
+        })}>
+        {(!loaded || !imageDimensions) && <ImageLoading />}
+        <TouchableWithoutFeedback
+          onPress={doubleTapToZoomEnabled ? handleDoubleTap : undefined}
+          onLongPress={onLongPressHandler}
+          delayLongPress={delayLongPress}>
+          <Animated.Image
+            source={imageSrc}
+            style={imageStylesWithOpacity}
+            onLoad={() => setLoaded(true)}
+          />
+        </TouchableWithoutFeedback>
+      </ScrollView>
+    </View>
+  )
+}
+
+const styles = StyleSheet.create({
+  listItem: {
+    width: SCREEN_WIDTH,
+    height: SCREEN_HEIGHT,
+  },
+  imageScrollContainer: {
+    height: SCREEN_HEIGHT,
+  },
+})
+
+export default React.memo(ImageItem)
diff --git a/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx
new file mode 100644
index 000000000..9667fcaa7
--- /dev/null
+++ b/src/view/com/lightbox/ImageViewing/components/ImageItem/ImageLoading.tsx
@@ -0,0 +1,37 @@
+/**
+ * Copyright (c) JOB TODAY S.A. and its affiliates.
+ *
+ * This source code is licensed under the MIT license found in the
+ * LICENSE file in the root directory of this source tree.
+ *
+ */
+
+import React from 'react'
+
+import {ActivityIndicator, Dimensions, StyleSheet, View} from 'react-native'
+
+const SCREEN = Dimensions.get('screen')
+const SCREEN_WIDTH = SCREEN.width
+const SCREEN_HEIGHT = SCREEN.height
+
+export const ImageLoading = () => (
+  <View style={styles.loading}>
+    <ActivityIndicator size="small" color="#FFF" />
+  </View>
+)
+
+const styles = StyleSheet.create({
+  listItem: {
+    width: SCREEN_WIDTH,
+    height: SCREEN_HEIGHT,
+  },
+  loading: {
+    width: SCREEN_WIDTH,
+    height: SCREEN_HEIGHT,
+    alignItems: 'center',
+    justifyContent: 'center',
+  },
+  imageScrollContainer: {
+    height: SCREEN_HEIGHT,
+  },
+})