From a8b4f55ae20246942388d4787d113b017afe0e94 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Altun?= Date: Mon, 6 Jun 2022 21:53:51 +0300 Subject: [PATCH] fix: story image scaling issues --- .../AutoHeightImage/AnimatableImage.js | 23 --- .../AutoHeightImage/AutoHeightImage.js | 84 --------- .../AutoHeightImage/ErrorableImage.js | 30 --- .../AutoHeightImage/ImagePolyfill.js | 33 ---- src/components/AutoHeightImage/cache.js | 83 --------- src/components/AutoHeightImage/helpers.js | 2 - src/components/AutoHeightImage/index.d.ts | 22 --- src/components/AutoHeightImage/index.js | 3 - src/components/StoryListItem.tsx | 171 +++++++++++------- 9 files changed, 105 insertions(+), 346 deletions(-) delete mode 100644 src/components/AutoHeightImage/AnimatableImage.js delete mode 100644 src/components/AutoHeightImage/AutoHeightImage.js delete mode 100644 src/components/AutoHeightImage/ErrorableImage.js delete mode 100644 src/components/AutoHeightImage/ImagePolyfill.js delete mode 100644 src/components/AutoHeightImage/cache.js delete mode 100644 src/components/AutoHeightImage/helpers.js delete mode 100644 src/components/AutoHeightImage/index.d.ts delete mode 100644 src/components/AutoHeightImage/index.js diff --git a/src/components/AutoHeightImage/AnimatableImage.js b/src/components/AutoHeightImage/AnimatableImage.js deleted file mode 100644 index 4eb82e7..0000000 --- a/src/components/AutoHeightImage/AnimatableImage.js +++ /dev/null @@ -1,23 +0,0 @@ -import React from 'react'; -import PropTypes from 'prop-types'; -import { Animated, Image, ImageBackground } from 'react-native'; - -function AnimatableImage(props) { - const { animated, children, ...rest } = props; - - const ImageComponent = children - ? ImageBackground - : animated - ? Animated.Image - : Image; - - return {children}; -} - -AnimatableImage.propTypes = Image.propTypes | Animated.Image.propTypes; - -AnimatableImage.defaultProps = { - animated: false, -}; - -export default AnimatableImage; diff --git a/src/components/AutoHeightImage/AutoHeightImage.js b/src/components/AutoHeightImage/AutoHeightImage.js deleted file mode 100644 index 2137d36..0000000 --- a/src/components/AutoHeightImage/AutoHeightImage.js +++ /dev/null @@ -1,84 +0,0 @@ -/** - * @since 2017-04-11 19:10:08 - * @author vivaxy - */ -import React, { useEffect, useState, useRef } from 'react'; -import ImagePolyfill from './ImagePolyfill'; -import AnimatableImage from './AnimatableImage'; -import PropTypes from 'prop-types'; - -import { getImageSizeFitWidth, getImageSizeFitWidthFromCache } from './cache'; -import { NOOP, DEFAULT_HEIGHT } from './helpers'; - -// remove `resizeMode` props from `Image.propTypes` -const { resizeMode, ...ImagePropTypes } = AnimatableImage.propTypes; - -function AutoHeightImage(props) { - const { onHeightChange, source, width, style, maxHeight, onError, ...rest } = - props; - const [height, setHeight] = useState( - getImageSizeFitWidthFromCache(source, width, maxHeight).height || - DEFAULT_HEIGHT - ); - const mountedRef = useRef(false); - - useEffect(function () { - mountedRef.current = true; - return function () { - mountedRef.current = false; - }; - }, []); - - useEffect( - function () { - (async function () { - try { - const { height: newHeight } = await getImageSizeFitWidth( - source, - width, - maxHeight - ); - if (mountedRef.current) { - // might trigger `onHeightChange` with same `height` value - // dedupe maybe? - setHeight(newHeight); - onHeightChange(newHeight); - } - } catch (e) { - onError(e); - } - })(); - }, - [source, onHeightChange, width, maxHeight] - ); - - // StyleSheet.create will cache styles, not what we want - const imageStyles = { width, height }; - - // Since it only makes sense to use polyfill with remote images - const ImageComponent = source.uri ? ImagePolyfill : AnimatableImage; - return ( - - ); -} - -AutoHeightImage.propTypes = { - ...ImagePropTypes, - width: PropTypes.number.isRequired, - maxHeight: PropTypes.number, - onHeightChange: PropTypes.func, - animated: PropTypes.bool, -}; - -AutoHeightImage.defaultProps = { - maxHeight: Infinity, - onHeightChange: NOOP, - animated: false, -}; - -export default AutoHeightImage; diff --git a/src/components/AutoHeightImage/ErrorableImage.js b/src/components/AutoHeightImage/ErrorableImage.js deleted file mode 100644 index be8e977..0000000 --- a/src/components/AutoHeightImage/ErrorableImage.js +++ /dev/null @@ -1,30 +0,0 @@ -import React, { useState } from 'react'; - -import AutoHeightImage from './AutoHeightImage'; - -function ErrorableImage(props) { - const { source, fallbackSource, onError, ...rest } = props; - - const [error, setError] = useState(false); - - const shouldUseFallbackSource = error && fallbackSource; - - return ( - { - // if an error hasn't already been seen, try to load the error image - // instead - if (!error) { - setError(true); - } - - // also propagate to error handler if it is specified - onError && onError(_e); - }} - {...rest} - /> - ); -} - -export default ErrorableImage; diff --git a/src/components/AutoHeightImage/ImagePolyfill.js b/src/components/AutoHeightImage/ImagePolyfill.js deleted file mode 100644 index f0018f4..0000000 --- a/src/components/AutoHeightImage/ImagePolyfill.js +++ /dev/null @@ -1,33 +0,0 @@ -import React, { useEffect } from 'react'; -import { Platform, Image } from 'react-native'; -import AnimatableImage from './AnimatableImage'; - -const isAndroid = () => Platform.OS === 'android'; - -/** - * An extension of the Image class which fixes an Android bug where remote images wouldn't fire the - * Image#onError() callback when the image failed to load due to a 404 response. - * - * This component should only be used for loading remote images, not local resources. - */ -function ImagePolyfill(props) { - const { source, onError, ...rest } = props; - - const verifyImage = () => { - const { uri } = source; - Image.prefetch(uri).catch((e) => onError(e)); - }; - - useEffect(() => { - if (source && source.uri && onError && isAndroid()) { - verifyImage(); - } - }, [source, onError]); - - return ; -} - -ImagePolyfill.propTypes = AnimatableImage.propTypes; -ImagePolyfill.defaultProps = AnimatableImage.defaultProps; - -export default ImagePolyfill; diff --git a/src/components/AutoHeightImage/cache.js b/src/components/AutoHeightImage/cache.js deleted file mode 100644 index 959a960..0000000 --- a/src/components/AutoHeightImage/cache.js +++ /dev/null @@ -1,83 +0,0 @@ -/** - * @since 2017-04-24 20:50:41 - * @author vivaxy - */ - -import { Image } from 'react-native'; -// undocumented but part of react-native; see -// https://github.com/facebook/react-native/issues/5603#issuecomment-297959695 -import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource'; - -/** - * store with - * key: image - * value: { - * width: 100, - * height: 100, - * } - */ -const cache = new Map(); - -const getImageSizeFromCache = (image) => { - if (typeof image === 'number') { - return cache.get(image); - } else { - return cache.get(image.uri); - } -}; - -const loadImageSize = (image) => { - return new Promise((resolve, reject) => { - //number indicates import X or require(X) was used (i.e. local file) - if (typeof image === 'number') { - const { width, height } = resolveAssetSource(image); - resolve({ width, height }); - } else { - Image.getSize( - image.uri, - (width, height) => { - // success - resolve({ width, height }); - }, - reject - ); - } - }); -}; - -export const getImageSizeFitWidthFromCache = (image, toWidth, maxHeight) => { - const size = getImageSizeFromCache(image); - if (size) { - const { width, height } = size; - if (!width || !height) return { width: 0, height: 0 }; - const scaledHeight = (toWidth * height) / width; - return { - width: toWidth, - height: scaledHeight > maxHeight ? maxHeight : scaledHeight, - }; - } - return {}; -}; - -const getImageSizeMaybeFromCache = async (image) => { - let size = getImageSizeFromCache(image); - if (!size) { - size = await loadImageSize(image); - if (typeof image === 'number') { - cache.set(image, size); - } else { - cache.set(image.uri, size); - } - } - return size; -}; - -export const getImageSizeFitWidth = async (image, toWidth, maxHeight) => { - const { width, height } = await getImageSizeMaybeFromCache(image); - if (!width || !height) return { width: 0, height: 0 }; - const scaledHeight = (toWidth * height) / width; - return { - width: toWidth, - height: scaledHeight > maxHeight ? maxHeight : scaledHeight, - }; -}; diff --git a/src/components/AutoHeightImage/helpers.js b/src/components/AutoHeightImage/helpers.js deleted file mode 100644 index cc61da7..0000000 --- a/src/components/AutoHeightImage/helpers.js +++ /dev/null @@ -1,2 +0,0 @@ -export const NOOP = () => {}; -export const DEFAULT_HEIGHT = 0; diff --git a/src/components/AutoHeightImage/index.d.ts b/src/components/AutoHeightImage/index.d.ts deleted file mode 100644 index cbcaa8f..0000000 --- a/src/components/AutoHeightImage/index.d.ts +++ /dev/null @@ -1,22 +0,0 @@ -import * as React from 'react'; -import { ImageProps } from 'react-native'; - -interface TSource { - uri: string; -} - -export interface AutoHeightImageProps extends ImageProps { - source: number | TSource; - width: number; - maxHeight?: number; - fallbackSource?: number | TSource; - onHeightChange?: (height: number) => void; - animated?: boolean; -} - -declare class AutoHeightImage extends React.Component< - AutoHeightImageProps, - any -> {} - -export default AutoHeightImage; diff --git a/src/components/AutoHeightImage/index.js b/src/components/AutoHeightImage/index.js deleted file mode 100644 index 7972cba..0000000 --- a/src/components/AutoHeightImage/index.js +++ /dev/null @@ -1,3 +0,0 @@ -import ErrorableImage from './ErrorableImage'; - -export default ErrorableImage; diff --git a/src/components/StoryListItem.tsx b/src/components/StoryListItem.tsx index 7615a7d..48c17f1 100644 --- a/src/components/StoryListItem.tsx +++ b/src/components/StoryListItem.tsx @@ -1,5 +1,11 @@ /* eslint-disable react-native/no-inline-styles */ -import React, { useState, useEffect, useRef, useMemo } from 'react'; +import React, { + useState, + useEffect, + useRef, + useMemo, + useCallback, +} from 'react'; import { Animated, Image, @@ -14,7 +20,6 @@ import { SafeAreaView, } from 'react-native'; import GestureRecognizer from 'react-native-swipe-gestures'; -import AutoHeightImage from './AutoHeightImage'; import { usePrevious } from '../helpers/StateHelpers'; import { isNullOrWhitespace } from '../helpers/ValidationHelpers'; @@ -49,8 +54,10 @@ type Props = { const StoryListItem = (props: Props) => { const [loading, setLoading] = useState(true); const [pressed, setPressed] = useState(false); - const [currStoryIndex, setCurrentStoryIndex] = useState(0); + const [currStoryIndex, setCurrStoryIndex] = useState(0); const [content, setContent] = useState(props.stories); + const [currImageWidth, setCurrImageWidth] = useState(0); + const [currImageHeight, setCurrImageHeight] = useState(0); const currStory = useMemo( () => content[currStoryIndex], @@ -69,14 +76,97 @@ const StoryListItem = (props: Props) => { const prevPageIndex = usePrevious(currPageIndex); const prevStoryIndex = usePrevious(currStoryIndex); + const close = useCallback( + (state: ActionStates) => { + let data = [...content]; + data.map((x) => (x.finished = false)); + setContent(data); + progress.setValue(0); + if (currPageIndex === props.index) { + if (props.onFinish) { + props.onFinish(state); + } + } + }, + [content, currPageIndex, progress, props] + ); + + const next = useCallback(() => { + // check if the next content is not empty + setLoading(true); + if (currStoryIndex !== content.length - 1) { + let data = [...content]; + data[currStoryIndex].finished = true; + setContent(data); + setCurrStoryIndex(currStoryIndex + 1); + progress.setValue(0); + } else { + // the next content is empty + close(ActionStates.NEXT); + } + }, [close, content, currStoryIndex, progress]); + + const previous = () => { + // checking if the previous content is not empty + setLoading(true); + if (currStoryIndex - 1 >= 0) { + let data = [...content]; + data[currStoryIndex].finished = false; + setContent(data); + setCurrStoryIndex(currStoryIndex - 1); + progress.setValue(0); + } else { + // the previous content is empty + close(ActionStates.PREVIOUS); + } + }; + + const startProgressAnimation = useCallback(() => { + Animated.timing(progress, { + toValue: 1, + duration: props.duration, + useNativeDriver: false, + }).start(({ finished }) => { + if (finished) next(); + }); + }, [next, progress, props.duration]); + + const startStory = useCallback(() => { + Image.getSize( + content[currStoryIndex].image, + (imageWidth, imageHeight) => { + let newHeight = imageHeight; + let newWidth = imageWidth; + + const isImageWidthBiggerThenPhone = imageWidth > width; + + if (isImageWidthBiggerThenPhone) { + newHeight = Number(imageHeight) + ? Math.floor(width * (imageHeight / imageWidth)) + : width; + newWidth = width; + } + + setCurrImageWidth(newWidth); + setCurrImageHeight(newHeight); + setLoading(false); + progress.setValue(0); + startProgressAnimation(); + }, + (errorMsg) => { + console.log(errorMsg); + } + ); + }, [content, currStoryIndex, progress, startProgressAnimation]); + // call every page changes useEffect(() => { const isPrevious = !!prevPageIndex && prevPageIndex > currPageIndex; if (isPrevious) { - setCurrentStoryIndex(content.length - 1); + setCurrStoryIndex(content.length - 1); } else { - setCurrentStoryIndex(0); + setCurrStoryIndex(0); } let data = [...content]; @@ -115,22 +205,6 @@ const StoryListItem = (props: Props) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, [currStoryIndex]); - const startProgressAnimation = () => { - Animated.timing(progress, { - toValue: 1, - duration: props.duration, - useNativeDriver: false, - }).start(({ finished }) => { - if (finished) next(); - }); - }; - - const startStory = () => { - setLoading(false); - progress.setValue(0); - startProgressAnimation(); - }; - const onSwipeUp = () => { if (props.onClosePress) props.onClosePress(); @@ -141,48 +215,6 @@ const StoryListItem = (props: Props) => { props?.onClosePress(); }; - const next = () => { - // check if the next content is not empty - setLoading(true); - if (currStoryIndex !== content.length - 1) { - let data = [...content]; - data[currStoryIndex].finished = true; - setContent(data); - setCurrentStoryIndex(currStoryIndex + 1); - progress.setValue(0); - } else { - // the next content is empty - close(ActionStates.NEXT); - } - }; - - const previous = () => { - // checking if the previous content is not empty - setLoading(true); - if (currStoryIndex - 1 >= 0) { - let data = [...content]; - data[currStoryIndex].finished = false; - setContent(data); - setCurrentStoryIndex(currStoryIndex - 1); - progress.setValue(0); - } else { - // the previous content is empty - close(ActionStates.PREVIOUS); - } - }; - - const close = (state: ActionStates) => { - let data = [...content]; - data.map((x) => (x.finished = false)); - setContent(data); - progress.setValue(0); - if (currPageIndex === props.index) { - if (props.onFinish) { - props.onFinish(state); - } - } - }; - const renderSwipeButton = () => { if (props.customSwipeUpButton) { return props.customSwipeUpButton(); @@ -224,10 +256,15 @@ const StoryListItem = (props: Props) => { }); return ( - ); @@ -336,6 +373,7 @@ const styles = StyleSheet.create({ backgroundColor: '#000', }, image: { + maxWidth: width, maxHeight: height - getStatusBarHeight(true), }, backgroundContainer: { @@ -345,6 +383,7 @@ const styles = StyleSheet.create({ left: 0, right: 0, justifyContent: 'center', + alignItems: 'center', }, spinnerContainer: { zIndex: -100,