diff --git a/native/media/media-gallery-media.react.js b/native/media/media-gallery-media.react.js index 024d005a2..cf1f31b03 100644 --- a/native/media/media-gallery-media.react.js +++ b/native/media/media-gallery-media.react.js @@ -1,310 +1,324 @@ // @flow import Icon from '@expo/vector-icons/FontAwesome.js'; import MaterialIcon from '@expo/vector-icons/MaterialIcons.js'; import LottieView from 'lottie-react-native'; import * as React from 'react'; import { TouchableOpacity, StyleSheet, View, Text, Platform, Animated, Easing, } from 'react-native'; import Reanimated, { EasingNode as ReanimatedEasing, + useValue, } from 'react-native-reanimated'; import Video from 'react-native-video'; import { type MediaLibrarySelection } from 'lib/types/media-types.js'; import GestureTouchableOpacity from '../components/gesture-touchable-opacity.react.js'; import { type DimensionsInfo } from '../redux/dimensions-updater.react.js'; import type { AnimatedValue } from '../types/react-native.js'; import { AnimatedView, AnimatedImage, type AnimatedViewStyle, type AnimatedStyleObj, } from '../types/styles.js'; const animatedSpec = { duration: 400, easing: Easing.inOut(Easing.ease), useNativeDriver: true, }; const reanimatedSpec = { duration: 400, easing: ReanimatedEasing.inOut(ReanimatedEasing.ease), }; type Props = { +selection: MediaLibrarySelection, +containerHeight: number, +queueModeActive: boolean, +isQueued: boolean, +setMediaQueued: (media: MediaLibrarySelection, isQueued: boolean) => void, +sendMedia: (media: MediaLibrarySelection) => void, +isFocused: boolean, +setFocus: (media: MediaLibrarySelection, isFocused: boolean) => void, +dimensions: DimensionsInfo, }; -class MediaGalleryMedia extends React.PureComponent { - focusProgress: Reanimated.Value = new Reanimated.Value(0); - buttonsStyle: AnimatedViewStyle; - mediaStyle: AnimatedStyleObj; - checkProgress: AnimatedValue = new Animated.Value(0); +function MediaGalleryMedia(props: Props): React.Node { + const { + selection, + containerHeight, + queueModeActive, + isQueued, + setMediaQueued, + sendMedia, + isFocused, + setFocus, + dimensions, + } = props; + const focusProgress: Reanimated.Value = useValue(0); + const checkProgress: AnimatedValue = React.useMemo( + () => new Animated.Value(0), + [], + ); - constructor(props: Props) { - super(props); - - const buttonsScale = Reanimated.interpolateNode(this.focusProgress, { + const buttonsStyle: AnimatedViewStyle = React.useMemo(() => { + const buttonsScale = Reanimated.interpolateNode(focusProgress, { inputRange: [0, 1], outputRange: [1.3, 1], }); - this.buttonsStyle = { + return { ...styles.buttons, - opacity: this.focusProgress, + opacity: focusProgress, transform: [{ scale: buttonsScale }], - marginBottom: this.props.dimensions.bottomInset, + marginBottom: dimensions.bottomInset, }; + }, [focusProgress, dimensions.bottomInset]); - const mediaScale = Reanimated.interpolateNode(this.focusProgress, { + const mediaStyle: AnimatedStyleObj = React.useMemo(() => { + const mediaScale = Reanimated.interpolateNode(focusProgress, { inputRange: [0, 1], outputRange: [1, 1.3], }); - this.mediaStyle = { + return { transform: [{ scale: mediaScale }], }; - } + }, [focusProgress]); - static isActive(props: Props): boolean { - return props.isFocused || props.isQueued; - } - - componentDidMount() { - const isActive = MediaGalleryMedia.isActive(this.props); - if (isActive) { - Reanimated.timing(this.focusProgress, { - ...reanimatedSpec, - toValue: 1, - }).start(); - } + const prevActivityStatus = React.useRef({ + isFocused: false, + isQueued: false, + }); - if (this.props.isQueued) { - // When I updated to React Native 0.60, I also updated Lottie. At that - // time, on iOS the last frame of the animation drops the circle outlining - // the checkmark. This is a hack to get around that - const maxValue = Platform.OS === 'ios' ? 0.99 : 1; - Animated.timing(this.checkProgress, { - ...animatedSpec, - toValue: maxValue, - }).start(); - } - } + React.useEffect(() => { + const isActive = isFocused || isQueued; + const wasActive = + prevActivityStatus.current.isFocused || + prevActivityStatus.current.isQueued; - componentDidUpdate(prevProps: Props) { - const isActive = MediaGalleryMedia.isActive(this.props); - const wasActive = MediaGalleryMedia.isActive(prevProps); if (isActive && !wasActive) { - Reanimated.timing(this.focusProgress, { + Reanimated.timing(focusProgress, { ...reanimatedSpec, toValue: 1, }).start(); } else if (!isActive && wasActive) { - Reanimated.timing(this.focusProgress, { + Reanimated.timing(focusProgress, { ...reanimatedSpec, toValue: 0, }).start(); } - if (this.props.isQueued && !prevProps.isQueued) { + if (isQueued && !prevActivityStatus.current.isQueued) { // When I updated to React Native 0.60, I also updated Lottie. At that // time, on iOS the last frame of the animation drops the circle outlining // the checkmark. This is a hack to get around that const maxValue = Platform.OS === 'ios' ? 0.99 : 1; - Animated.timing(this.checkProgress, { + Animated.timing(checkProgress, { ...animatedSpec, toValue: maxValue, }).start(); - } else if (!this.props.isQueued && prevProps.isQueued) { - Animated.timing(this.checkProgress, { + } else if (!isQueued && prevActivityStatus.current.isQueued) { + Animated.timing(checkProgress, { ...animatedSpec, toValue: 0, }).start(); } - } - render(): React.Node { - const { selection, containerHeight } = this.props; - const { - uri, - dimensions: { width, height }, - step, - } = selection; - const active = MediaGalleryMedia.isActive(this.props); - const scaledWidth = height ? (width * containerHeight) / height : 0; - const dimensionsStyle: { +height: number, +width: number } = { - height: containerHeight, - width: Math.max(Math.min(scaledWidth, this.props.dimensions.width), 150), + prevActivityStatus.current = { + isFocused: isFocused, + isQueued: isQueued, }; + }, [checkProgress, focusProgress, isFocused, isQueued]); - let buttons = null; - const { queueModeActive } = this.props; - if (!queueModeActive) { - buttons = ( - <> - - - Send - - - - Queue - - - ); - } - - let media; - const source = { uri }; - if (step === 'video_library') { - let resizeMode = 'contain'; - if (Platform.OS === 'ios') { - const [major, minor] = Platform.Version.split('.'); - if (parseInt(major, 10) === 14 && parseInt(minor, 10) < 2) { - resizeMode = 'stretch'; - } - } - media = ( - - - ); + const onPressBackdrop = React.useCallback(() => { + if (isQueued) { + setMediaQueued(selection, false); + } else if (queueModeActive) { + setMediaQueued(selection, true); } else { - media = ( - - ); + setFocus(selection, !isFocused); } + }, [ + isQueued, + queueModeActive, + setMediaQueued, + selection, + setFocus, + isFocused, + ]); - const overlay = ( - - - - ); + const onPressSend = React.useCallback(() => { + sendMedia(selection); + }, [selection, sendMedia]); + + const onPressEnqueue = React.useCallback(() => { + setMediaQueued(selection, true); + }, [selection, setMediaQueued]); + + const { + uri, + dimensions: { width, height }, + step, + } = selection; + const active = isFocused || isQueued; + + const dimensionsStyle: { +height: number, +width: number } = + React.useMemo(() => { + const scaledWidth = height ? (width * containerHeight) / height : 0; + + return { + height: containerHeight, + width: Math.max(Math.min(scaledWidth, width), 150), + }; + }, [containerHeight, height, width]); - return ( - - + - {media} - - + Send + + - {buttons} - - + + Queue + + ); } - onPressBackdrop: () => void = () => { - if (this.props.isQueued) { - this.props.setMediaQueued(this.props.selection, false); - } else if (this.props.queueModeActive) { - this.props.setMediaQueued(this.props.selection, true); - } else { - this.props.setFocus(this.props.selection, !this.props.isFocused); + const animatedImageStyle = React.useMemo( + () => [mediaStyle, dimensionsStyle], + [dimensionsStyle, mediaStyle], + ); + + let media; + const source = { uri }; + if (step === 'video_library') { + let resizeMode = 'contain'; + if (Platform.OS === 'ios') { + const [major, minor] = Platform.Version.split('.'); + if (parseInt(major, 10) === 14 && parseInt(minor, 10) < 2) { + resizeMode = 'stretch'; + } } - }; + media = ( + + + ); + } else { + media = ; + } - onPressSend: () => void = () => { - this.props.sendMedia(this.props.selection); - }; + const overlay = ( + + + + ); - onPressEnqueue: () => void = () => { - this.props.setMediaQueued(this.props.selection, true); - }; + const containerStyle = React.useMemo( + () => [styles.container, dimensionsStyle], + [dimensionsStyle], + ); + + return ( + + + {media} + + + {buttons} + + + ); } const buttonStyle = { flexDirection: 'row', alignItems: 'flex-start', margin: 10, borderRadius: 20, paddingLeft: 20, paddingRight: 20, paddingTop: 10, paddingBottom: 10, }; const styles = StyleSheet.create({ buttonIcon: { alignSelf: Platform.OS === 'android' ? 'center' : 'flex-end', color: 'white', fontSize: 18, marginRight: 6, paddingRight: 5, }, buttonText: { color: 'white', fontSize: 16, }, buttons: { alignItems: 'center', bottom: 0, justifyContent: 'center', left: 0, position: 'absolute', right: 0, top: 0, }, checkAnimation: { position: 'absolute', width: 128, }, container: { flex: 1, overflow: 'hidden', }, enqueueButton: { ...buttonStyle, backgroundColor: '#2A78E5', }, sendButton: { ...buttonStyle, backgroundColor: '#7ED321', paddingLeft: 18, }, }); export default MediaGalleryMedia;