/* eslint-disable react-native/no-inline-styles */ import React, { useState, useEffect, useRef, useMemo, useCallback, } from 'react'; import { Animated, Image, Text, TouchableOpacity, StyleSheet, Dimensions, TouchableWithoutFeedback, ActivityIndicator, View, Platform, SafeAreaView, } from 'react-native'; import GestureRecognizer from 'react-native-swipe-gestures'; import usePrevious from '../helpers/usePrevious'; import { isNullOrWhitespace } from '../helpers/ValidationHelpers'; import { ActionStates } from '../index'; import StoryImage from './StoryImage'; import type { UserStoryItem, CustomProfileBanner, CustomStoryImage, } from '../index'; import AnimationBar from './AnimationBar'; const { width, height } = Dimensions.get('window'); interface StoryListItemProps { index: number; profileName: string; profileImage: any; // @todo duration?: number; onFinish?: (state: ActionStates) => void; onClosePress: () => void; swipeText?: string; customSwipeUpButton?: () => React.ReactNode; customCloseButton?: () => React.ReactNode; customProfileBanner?: (props: CustomProfileBanner) => React.ReactNode; customStoryImage?: (props: CustomStoryImage) => React.ReactNode; stories: UserStoryItem[]; showProfileBanner?: boolean; currentPage: number; } const StoryListItem = (props: StoryListItemProps) => { const [loading, setLoading] = useState(true); const [pressed, setPressed] = useState(false); 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], [content, currStoryIndex] ) as UserStoryItem; const currPageIndex = useMemo(() => props.currentPage, [props.currentPage]); const swipeText = useMemo( () => content?.[currStoryIndex]?.swipeText || props.swipeText || 'Swipe Up', [content, currStoryIndex, props.swipeText] ); const progress = useRef(new Animated.Value(0)).current; 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] as UserStoryItem).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] as UserStoryItem).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] as UserStoryItem).image, (imageWidth, imageHeight) => { let newHeight = imageHeight; let newWidth = imageWidth; const isImageWidthBiggerThenPhone = imageWidth > width; if (isImageWidthBiggerThenPhone) { newWidth = width; newHeight = imageHeight ? Math.floor(width * (imageHeight / imageWidth)) : width; } const isNewHeightBiggerThenPhone = newHeight > height; if (isNewHeightBiggerThenPhone) { newWidth = height * (imageWidth / imageHeight); newHeight = height; } 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) { setCurrStoryIndex(content.length - 1); } else { setCurrStoryIndex(0); } let data = [...content]; data.map((x, i) => { if (isPrevious) { x.finished = true; if (i === content.length - 1) { x.finished = false; } } else { x.finished = false; } }); setContent(data); startStory(); // eslint-disable-next-line react-hooks/exhaustive-deps }, [currPageIndex]); // call every story change requests // ... and decide next or prev useEffect(() => { if (!isNullOrWhitespace(prevStoryIndex)) { const isNextStory = !!prevStoryIndex && currStoryIndex > prevStoryIndex; const isPrevStory = !isNextStory; const nextStory = content[currStoryIndex + 1]; const prevStory = content[currStoryIndex - 1]; if (isNextStory && (prevStory as UserStoryItem).id === currStory.id) { startStory(); } else if (isPrevStory && nextStory?.id === currStory.id) { startStory(); } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [currStoryIndex]); const onSwipeUp = () => { if (props.onClosePress) props.onClosePress(); if (currStory.onPress) currStory.onPress(); }; const onSwipeDown = () => { props?.onClosePress(); }; const renderSwipeButton = () => { if (props.customSwipeUpButton) { return props.customSwipeUpButton(); } return {swipeText}; }; const renderCloseButton = () => { if (props.customCloseButton) { return props.customCloseButton(); } return X; }; const renderProfileBanner = () => { if (!props.showProfileBanner) return; if (props.customProfileBanner) return props.customProfileBanner({ image: props.profileImage, name: props.profileName, }); return ( <> {props.profileName} ); }; const renderStoryImage = () => { if (props.customStoryImage) return props.customStoryImage({ image: currStory.image, onLoadEnd: startStory, imageWidth: currImageWidth, imageHeight: currImageHeight, }); return ( ); }; return ( {renderStoryImage()} {loading && ( )} {renderProfileBanner()} { if (props.onClosePress) { props.onClosePress(); } }} > {renderCloseButton()} progress.stopAnimation()} onLongPress={() => setPressed(true)} onPressOut={() => { setPressed(false); startProgressAnimation(); }} onPress={() => { if (!pressed && !loading) previous(); }} > progress.stopAnimation()} onLongPress={() => setPressed(true)} onPressOut={() => { setPressed(false); startProgressAnimation(); }} onPress={() => { if (!pressed && !loading) next(); }} > {currStory.onPress && ( {renderSwipeButton()} )} ); }; StoryListItem.defaultProps = { duration: 10000, }; const styles = StyleSheet.create({ container: { flex: 1, backgroundColor: '#000', }, backgroundContainer: { position: 'absolute', top: 0, bottom: 0, left: 0, right: 0, justifyContent: 'center', alignItems: 'center', }, spinnerContainer: { zIndex: -100, position: 'absolute', justifyContent: 'center', backgroundColor: '#000', alignSelf: 'center', width: width, height: height, }, content: { flexDirection: 'column', flex: 1, }, userContainer: { height: 50, flexDirection: 'row', justifyContent: 'space-between', paddingHorizontal: 15, }, avatarImage: { height: 30, width: 30, borderRadius: 100, }, avatarText: { fontWeight: 'bold', color: '#FFF', paddingLeft: 10, }, closeIconContainer: { alignItems: 'center', justifyContent: 'center', height: 50, paddingHorizontal: 15, }, pressContainer: { flex: 1, flexDirection: 'row', }, swipeUpBtn: { position: 'absolute', right: 0, left: 0, alignItems: 'center', bottom: Platform.OS === 'ios' ? 20 : 50, }, swipeText: { color: '#FFF', marginTop: 10, }, closeText: { color: '#FFF', }, profileContainer: { flexDirection: 'row', alignItems: 'center', }, }); export default StoryListItem;