react-native-story-component
Version:
Story component for React Native.
342 lines (340 loc) • 10.7 kB
JavaScript
/* 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 AnimationBar from './AnimationBar';
const {
width,
height
} = Dimensions.get('window');
const StoryListItem = props => {
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]);
const currPageIndex = useMemo(() => props.currentPage, [props.currentPage]);
const swipeText = useMemo(() => {
var _content$currStoryInd;
return (content === null || content === void 0 ? void 0 : (_content$currStoryInd = content[currStoryIndex]) === null || _content$currStoryInd === void 0 ? void 0 : _content$currStoryInd.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 => {
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(_ref => {
let {
finished
} = _ref;
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) {
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.id === currStory.id) {
startStory();
} else if (isPrevStory && (nextStory === null || nextStory === void 0 ? void 0 : 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 === null || props === void 0 ? void 0 : props.onClosePress();
};
const renderSwipeButton = () => {
if (props.customSwipeUpButton) {
return props.customSwipeUpButton();
}
return /*#__PURE__*/React.createElement(Text, {
style: styles.swipeText
}, swipeText);
};
const renderCloseButton = () => {
if (props.customCloseButton) {
return props.customCloseButton();
}
return /*#__PURE__*/React.createElement(Text, {
style: styles.closeText
}, "X");
};
const renderProfileBanner = () => {
if (!props.showProfileBanner) return;
if (props.customProfileBanner) return props.customProfileBanner({
image: props.profileImage,
name: props.profileName
});
return /*#__PURE__*/React.createElement(React.Fragment, null, /*#__PURE__*/React.createElement(Image, {
style: styles.avatarImage,
source: props.profileImage
}), /*#__PURE__*/React.createElement(Text, {
style: styles.avatarText
}, props.profileName));
};
const renderStoryImage = () => {
if (props.customStoryImage) return props.customStoryImage({
image: currStory.image,
onLoadEnd: startStory,
imageWidth: currImageWidth,
imageHeight: currImageHeight
});
return /*#__PURE__*/React.createElement(StoryImage, {
source: {
uri: currStory.image
},
width: currImageWidth,
height: currImageHeight,
onLoadEnd: startStory
});
};
return /*#__PURE__*/React.createElement(GestureRecognizer, {
onSwipeUp: onSwipeUp,
onSwipeDown: onSwipeDown,
config: {
velocityThreshold: 0.3,
directionalOffsetThreshold: 80
},
style: styles.container
}, /*#__PURE__*/React.createElement(SafeAreaView, {
style: styles.backgroundContainer
}, renderStoryImage(), loading && /*#__PURE__*/React.createElement(View, {
style: styles.spinnerContainer
}, /*#__PURE__*/React.createElement(ActivityIndicator, {
size: "large",
color: "#FFF"
}))), /*#__PURE__*/React.createElement(View, {
style: styles.content
}, /*#__PURE__*/React.createElement(AnimationBar, {
currStoryIndex: currStoryIndex,
stories: content,
progress: progress
}), /*#__PURE__*/React.createElement(View, {
style: styles.userContainer
}, /*#__PURE__*/React.createElement(View, {
style: styles.profileContainer
}, renderProfileBanner()), /*#__PURE__*/React.createElement(TouchableOpacity, {
onPress: () => {
if (props.onClosePress) {
props.onClosePress();
}
}
}, /*#__PURE__*/React.createElement(View, {
style: styles.closeIconContainer
}, renderCloseButton()))), /*#__PURE__*/React.createElement(View, {
style: styles.pressContainer
}, /*#__PURE__*/React.createElement(TouchableWithoutFeedback, {
onPressIn: () => progress.stopAnimation(),
onLongPress: () => setPressed(true),
onPressOut: () => {
setPressed(false);
startProgressAnimation();
},
onPress: () => {
if (!pressed && !loading) previous();
}
}, /*#__PURE__*/React.createElement(View, {
style: {
flex: 0.3
}
})), /*#__PURE__*/React.createElement(TouchableWithoutFeedback, {
onPressIn: () => progress.stopAnimation(),
onLongPress: () => setPressed(true),
onPressOut: () => {
setPressed(false);
startProgressAnimation();
},
onPress: () => {
if (!pressed && !loading) next();
}
}, /*#__PURE__*/React.createElement(View, {
style: {
flex: 0.7
}
})))), currStory.onPress && /*#__PURE__*/React.createElement(TouchableOpacity, {
activeOpacity: 1,
onPress: onSwipeUp,
style: styles.swipeUpBtn
}, 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;
//# sourceMappingURL=StoryListItem.js.map