Skip to content

Commit

Permalink
feat: add support for ordering stories, move to next when video ends
Browse files Browse the repository at this point in the history
  • Loading branch information
JonatanSalas committed Oct 26, 2020
1 parent 0d2d65f commit fc9fd6f
Show file tree
Hide file tree
Showing 6 changed files with 153 additions and 62 deletions.
46 changes: 44 additions & 2 deletions src/components/StoryDetail/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,21 +14,54 @@ import * as instaEffect from './animations';
import { styles } from './styles';

export type Story = {
/**
* The ID of the story
*/
id: string | number | any;
/**
* The URL to the video
*/
video: string;
/**
* The URL to the avatar image
*/
preview: string;
/**
* A flag to mark the Story as visualized
*/
viewed: boolean;
};

export type StoreDetailProps = {
/**
* The initial index of the Story to present
*/
initial: number;
/**
* An array of stories to render
*/
stories: Story[];
/**
* A prop to mark if we need to show the Story Detail
*/
isVisible: boolean;
/**
* A back button handler callback
*/
onBackPress: (idx: number) => any;
/**
* Callback fired when we move to the next story
*/
onMoveToNextStory: (idx: number) => any;
/**
* A component to render as the Header of the Story Detail Item
*/
StoryDetailItemHeader?: (
props?: StoryDetailHeaderProps
) => React.ReactElement | null;
/**
* A component to render as the Footer of the Story Detail Item
*/
StoryDetailItemFooter?: (
props?: StoryDetailFooterProps
) => React.ReactElement | null;
Expand Down Expand Up @@ -72,20 +105,29 @@ export const StoryDetail: React.FC<StoreDetailProps> = ({
onBackdropPress={() => onBackPress(initial)}
>
<Carousel
useScrollView
data={stories}
ref={carouselRef}
itemWidth={width}
sliderWidth={width}
initialScrollIndex={initial}
scrollInterpolator={instaEffect.scrollInterpolator}
slideInterpolatedStyle={instaEffect.animatedStyles}
onSnapToItem={(idx) => onMoveToNextStory(idx)}
onBeforeSnapToItem={(idx) => onMoveToNextStory(idx)}
renderItem={({ item: story, index: idx }) => (
<StoryDetailItem
{...story}
isCurrentStory={initial === idx}
onBackPress={() => onBackPress(idx)}
onVideoEnd={() => {
if (idx <= stories.length - 2) {
setTimeout(
() => carouselRef.current.snapToItem(idx + 1, true, true),
250
);
} else {
onBackPress(idx);
}
}}
StoryDetailItemFooter={StoryDetailItemFooter}
StoryDetailItemHeader={StoryDetailItemHeader}
/>
Expand Down
45 changes: 38 additions & 7 deletions src/components/StoryDetailItem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,39 +1,68 @@
import Video from 'react-native-video';
import React, { useEffect, useState } from 'react';

import type { ImageURISource } from 'react-native';

import { styles } from './styles';

export type StoryDetailHeaderProps = {
/**
* A function to exit from the StoryDetail
*/
goBack: () => any;
/**
* A callback to mute audio from video
*/
mute: () => any;
/**
* It indicates if the video is muted or not
*/
muted: boolean;
};

export type StoryDetailFooterProps = {
/**
* The duration of the video been rendered
*/
videoDuration: number | string | null;
/**
* The progress of the video been rendered
*/
videoProgress: number | string | null;
};

export type StoryDetailItemProps = {
next?: any;
id?: string;
/**
* The URL to the video
*/
video?: string;
viewed?: boolean;
onVideoEnd?: any;
/**
* A boolean prop to enable play
*/
isCurrentStory: boolean;
preview?: string | ImageURISource;
/**
* Callback fired when video ends
*/
onVideoEnd: () => any;
/**
* A back button handler
*/
onBackPress: () => any;
/**
* A component to render as the Header of the Story Detail Item
*/
StoryDetailItemHeader: (
props?: StoryDetailHeaderProps
) => React.ReactElement | null;
/**
* A component to render as the Footer of the Story Detail Item
*/
StoryDetailItemFooter: (
props?: StoryDetailFooterProps
) => React.ReactElement | null;
};

export const StoryDetailItem: React.FC<StoryDetailItemProps> = ({
video,
onVideoEnd,
onBackPress,
isCurrentStory,
StoryDetailItemHeader,
Expand All @@ -56,6 +85,7 @@ export const StoryDetailItem: React.FC<StoryDetailItemProps> = ({
return (
<>
<StoryDetailItemHeader
muted={muted}
goBack={() => onBackPress()}
mute={() => setMuted(!muted)}
/>
Expand All @@ -71,6 +101,7 @@ export const StoryDetailItem: React.FC<StoryDetailItemProps> = ({
setDuration(nativeEvent.duration);
}}
onProgress={(nativeEvent: any) => setProgress(nativeEvent.currentTime)}
onEnd={() => onVideoEnd()}
/>
<StoryDetailItemFooter
videoProgress={progress}
Expand Down
31 changes: 30 additions & 1 deletion src/components/StoryPreview/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,21 @@ import type {
StoryDetailHeaderProps,
} from '../StoryDetailItem';

export type StoryPreviewItemProps = {
/**
* Size for the Avatar component
*/
size: 'small' | 'medium' | 'large' | 'xlarge' | number;
/**
* Styles for the container view
*/
containerStyle: ViewStyle;
/**
* Styles for the placeholder component
*/
placeholderStyle: ViewStyle;
};

export type StoryPreviewProps = {
/**
* An array of stories to be rendered
Expand All @@ -20,6 +35,10 @@ export type StoryPreviewProps = {
* Styles for FlatList mini stories container
*/
style?: ViewStyle;
/**
* Props for Story Preview Item component
*/
StoryPreviewItemProps?: StoryPreviewItemProps;
/**
* Callback fired when drag to next item
*/
Expand Down Expand Up @@ -49,6 +68,7 @@ export type StoryPreviewProps = {
export const StoryPreview: React.FC<StoryPreviewProps> = ({
style,
stories,
StoryPreviewItemProps,
StoryDetailItemHeader,
StoryDetailItemFooter,
onStoryDetailItemNext,
Expand All @@ -58,16 +78,22 @@ export const StoryPreview: React.FC<StoryPreviewProps> = ({
const [isVisible, setIsVisible] = useState<boolean>(false);
const [index, setIndex] = useState<any>(null);

const sortedStories = [
...stories.filter((story: Story) => story && !story.viewed),
...stories.filter((story: Story) => story && story.viewed),
];

return (
<>
<FlatList
horizontal
data={stories}
data={sortedStories}
showsHorizontalScrollIndicator={false}
keyExtractor={(story) => `${story.id}`}
contentContainerStyle={[styles.container, style]}
renderItem={({ item: story, index: idx }) => (
<StoryPreviewItem
{...StoryPreviewItemProps}
{...story}
onPress={() => {
setIsVisible(true);
Expand Down Expand Up @@ -115,3 +141,6 @@ export const StoryPreview: React.FC<StoryPreviewProps> = ({
};

StoryPreview.displayName = 'StoryPreview';
StoryPreview.defaultProps = {
stories: [],
};
74 changes: 38 additions & 36 deletions src/components/StoryPreviewItem/index.tsx
Original file line number Diff line number Diff line change
@@ -1,49 +1,51 @@
import React from 'react';
import { Avatar } from 'react-native-elements';
import { ActivityIndicator } from 'react-native';
import { ActivityIndicator, ViewStyle } from 'react-native';

import { styles } from './styles';

export type StoryPreviewItemProps = {
onPress: any;
/**
* URL of the avatar
*/
preview: string;
viewed: boolean;
};

// TODO: extract to props
const colors = {
primary: '#5D8FDB',
lightPrimary: 'rgb(233,240,251)',
secondary: '#5E5E5E',
storyBorder: '#D3D3D3',
lightSecondary: 'rgb(211,211,211)',
white: 'rgb(255,255,255)',
black: 'rgb(0,0,0)',
facebook: '#3b5998',
red: 'red',
/**
* The onPress handler
*/
onPress: () => void;
/**
* The styles to be applied to the container
*/
containerStyle?: ViewStyle;
/**
* The styles to be applied to the placeholder
*/
placeholderStyle?: ViewStyle;
/**
* The size of the Avatar component
*/
size?: 'small' | 'medium' | 'large' | 'xlarge' | number;
};

export const StoryPreviewItem: React.FC<StoryPreviewItemProps> = ({
viewed,
size,
onPress,
preview,
}) => {
return (
<Avatar
size="medium"
source={{
uri: preview,
}}
containerStyle={{
...styles.container,
borderColor: viewed ? colors.storyBorder : colors.red,
}}
onPress={() => onPress && onPress()}
renderPlaceholderContent={<ActivityIndicator color={colors.white} />}
placeholderStyle={styles.placeholder}
rounded
/>
);
};
preview: uri,
containerStyle,
placeholderStyle,
}) => (
<Avatar
rounded
size={size}
source={{ uri }}
onPress={() => onPress()}
placeholderStyle={placeholderStyle}
containerStyle={[styles.container, containerStyle]}
renderPlaceholderContent={<ActivityIndicator color="#FFFFFF" />}
/>
);

StoryPreviewItem.displayName = 'StoryPreviewItem';
StoryPreviewItem.defaultProps = {
size: 'medium',
};
16 changes: 0 additions & 16 deletions src/components/StoryPreviewItem/styles.ts
Original file line number Diff line number Diff line change
@@ -1,25 +1,9 @@
import { StyleSheet } from 'react-native';

// TODO: extract to props
const colors = {
primary: '#5D8FDB',
lightPrimary: 'rgb(233,240,251)',
secondary: '#5E5E5E',
storyBorder: '#D3D3D3',
lightSecondary: 'rgb(211,211,211)',
white: 'rgb(255,255,255)',
black: 'rgb(0,0,0)',
facebook: '#3b5998',
red: 'red',
};

export const styles = StyleSheet.create({
container: {
marginHorizontal: 10,
marginBottom: 18,
borderWidth: 1,
},
placeholder: {
backgroundColor: colors.primary,
},
});
3 changes: 3 additions & 0 deletions src/index.tsx
Original file line number Diff line number Diff line change
@@ -1 +1,4 @@
export * from './components/StoryPreview';

// TODO: add support for passing a loading component to the StoryDetailItem
// TODO: add support for improving the loading performance of react-native-video

0 comments on commit fc9fd6f

Please sign in to comment.