Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix/bugs #76

Merged
merged 7 commits into from
May 27, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 6 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,8 +60,8 @@ const YourComponent = () => {
name: 'User 1',
imgUrl: 'user1-profile-image-url',
stories: [
{ id: 'story1', sourceUrl: 'story1-image-url' },
{ id: 'story2', sourceUrl: 'story1-video-url', mediaType: 'video' },
{ id: 'story1', source: { uri: 'story1-image-url' } },
{ id: 'story2', source: { uri: 'story1-video-url' }, mediaType: 'video' },
// ...
]}, // ...
];
Expand Down Expand Up @@ -103,6 +103,7 @@ export default YourComponent;
`progressColor` | string | '#00000099' | Background color of progress bar item in inactive state
`progressActiveColor` | string | '#FFFFFF' | Background color of progress bar item in active state
`modalAnimationDuration` | number | 800 | Duration of modal animation in ms (showing/closing instagram stories)
`storyAnimationDuration` | number | 800 | Duration of story animation (animation when swiping to the left/right)
`mediaContainerStyle` | ViewStyle | | Additional styles for media (video or image) container
`imageStyles` | ImageStyle | { width: WIDTH, aspectRatio: 0.5626 } | Additional styles image component
`imageProps` | ImageProps | | Additional props applied to image component
Expand All @@ -111,6 +112,7 @@ export default YourComponent;
`headerContainerStyle` | ViewStyle | | Additional styles for the story header container
`progressContainerStyle` | ViewStyle | | Additional styles for the story progress container
`hideAvatarList` | boolean | false | A boolean indicating whether to hide avatar scroll list
`hideElementsOnLongPress` | boolean | false | A boolean indicating whether to hide all elements when story is paused by long press
`imageOverlayView` | ReactNode | | Image overlay compontent
`onShow` | ( id: string ) => void | | Callback when a story is shown.
`onHide` | ( id: string ) => void | | Callback when a story is hidden.
Expand Down Expand Up @@ -154,8 +156,9 @@ export default YourComponent;
Parameter | Type | Required
-----------------------|------------------------------------------|-------------------
`id` | string | true
`sourceUrl` | string | true
`source` | ImageProps['source'] | true
`mediaType` | 'video' \| 'image' (default: `'image'`) | false
`animationDuration` | number | false
`renderContent` | () => ReactNode | false
`renderFooter` | () => ReactNode | false

Expand Down
38 changes: 18 additions & 20 deletions src/components/Image/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,19 +8,20 @@ import Loader from '../Loader';
import { HEIGHT, LOADER_COLORS, WIDTH } from '../../core/constants';
import ImageStyles from './Image.styles';
import StoryVideo from './video';
import { StoryItemProps } from '~/core/dto/instagramStoriesDTO';

const StoryImage: FC<StoryImageProps> = ( {
stories, activeStory, defaultImage, isDefaultVideo, paused, videoProps, isActive,
mediaContainerStyle, imageStyles, imageProps, onImageLayout, onLoad,
stories, activeStory, defaultStory, isDefaultVideo, paused, videoProps, isActive,
mediaContainerStyle, imageStyles, imageProps, videoDuration, onImageLayout, onLoad,
} ) => {

const [ data, setData ] = useState<{ uri: string | undefined, isVideo?: boolean }>(
{ uri: defaultImage, isVideo: isDefaultVideo },
const [ data, setData ] = useState<{ data?: StoryItemProps, isVideo?: boolean }>(
{ data: defaultStory, isVideo: isDefaultVideo },
);

const loading = useSharedValue( true );
const color = useSharedValue( LOADER_COLORS );
const videoDuration = useSharedValue<number | undefined>( undefined );
const duration = useSharedValue<number | undefined>( undefined );
const isPaused = useDerivedValue( () => paused.value || !isActive.value );

const onImageChange = async () => {
Expand All @@ -39,26 +40,26 @@ const StoryImage: FC<StoryImageProps> = ( {

}

if ( data.uri === story.sourceUrl ) {
if ( data.data?.id === story.id ) {

if ( !loading.value ) {

onLoad( videoDuration.value );
onLoad( duration.value );

}

} else {

loading.value = true;
setData( { uri: story.sourceUrl, isVideo: story.mediaType === 'video' } );
setData( { data: story, isVideo: story.mediaType === 'video' } );

}

const nextStory = stories[stories.indexOf( story ) + 1];

if ( nextStory && nextStory.mediaType !== 'video' ) {
if ( nextStory && nextStory.mediaType !== 'video' && ( ( nextStory.source as any )?.uri || nextStory.sourceUrl ) ) {

Image.prefetch( nextStory.sourceUrl );
Image.prefetch( ( nextStory.source as any )?.uri ?? nextStory.sourceUrl );

}

Expand All @@ -76,19 +77,16 @@ const StoryImage: FC<StoryImageProps> = ( {
[ activeStory.value ],
);

const onContentLoad = ( duration?: number ) => {
const onContentLoad = ( newDuration?: number ) => {

if ( data.isVideo ) {

videoDuration.value = duration;

}
const animationDuration = ( data?.data?.mediaType === 'video' ? videoDuration : undefined ) ?? data.data?.animationDuration ?? newDuration;
duration.value = animationDuration;

loading.value = false;

if ( isActive.value ) {

onLoad( duration );
onLoad( animationDuration );

}

Expand All @@ -100,19 +98,19 @@ const StoryImage: FC<StoryImageProps> = ( {
<Loader loading={loading} color={color} size={50} />
</View>
<View style={[ ImageStyles.image, mediaContainerStyle ]}>
{data.uri && (
{( data.data?.source || data.data?.sourceUrl ) && (
data.isVideo ? (
<StoryVideo
onLoad={onContentLoad}
onLayout={onImageLayout}
uri={data.uri}
source={data.data.source ?? { uri: data.data.sourceUrl }}
paused={isPaused}
isActive={isActive}
{...videoProps}
/>
) : (
<Image
source={{ uri: data.uri }}
source={data.data.source ?? { uri: data.data.sourceUrl }}
style={[ { width: WIDTH, aspectRatio: 0.5626 }, imageStyles ]}
resizeMode="contain"
testID="storyImageComponent"
Expand Down
17 changes: 11 additions & 6 deletions src/components/Image/video.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { StoryVideoProps } from '../../core/dto/componentsDTO';
import { WIDTH } from '../../core/constants';

const StoryVideo: FC<StoryVideoProps> = ( {
uri, paused, isActive, onLoad, onLayout, ...props
source, paused, isActive, onLoad, onLayout, ...props
} ) => {

try {
Expand All @@ -17,13 +17,18 @@ const StoryVideo: FC<StoryVideoProps> = ( {

const ref = useRef<any>( null );

const [ pausedValue, setPausedValue ] = useState( !paused.value );
const [ pausedValue, setPausedValue ] = useState( paused.value );

const start = () => ref.current?.seek( 0 );
const start = () => {

ref.current?.seek( 0 );
ref.current?.resume();

};

useAnimatedReaction(
() => paused.value,
( res, prev ) => res !== prev && runOnJS( setPausedValue )( !res ),
( res, prev ) => res !== prev && runOnJS( setPausedValue )( res ),
[ paused.value ],
);

Expand All @@ -38,8 +43,8 @@ const StoryVideo: FC<StoryVideoProps> = ( {
ref={ref}
style={{ width: WIDTH, aspectRatio: 0.5626 }}
{...props}
source={{ uri }}
paused={!pausedValue}
source={source}
paused={pausedValue}
controls={false}
repeat={false}
onLoad={( { duration }: { duration: number } ) => onLoad( duration * 1000 )}
Expand Down
2 changes: 1 addition & 1 deletion src/components/InstagramStories/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,7 @@ const InstagramStories = forwardRef<InstagramStoriesPublicMethods, InstagramStor

}

return seenStory.mediaType !== 'video' ? Image.prefetch( seenStory.sourceUrl ) : true;
return seenStory.mediaType !== 'video' ? Image.prefetch( ( seenStory.source as any )?.uri ?? seenStory.sourceUrl ) : true;

} );

Expand Down
7 changes: 7 additions & 0 deletions src/components/List/List.styles.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,11 @@ export default StyleSheet.create( {
overflow: 'hidden',
width: WIDTH,
},
content: {
position: 'absolute',
top: 0,
left: 0,
bottom: 0,
right: 0,
},
} );
39 changes: 24 additions & 15 deletions src/components/List/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
import React, { FC, memo } from 'react';
import Animated, { useAnimatedStyle, useDerivedValue, useSharedValue } from 'react-native-reanimated';
import Animated, {
useAnimatedStyle, useDerivedValue, useSharedValue, withTiming,
} from 'react-native-reanimated';
import StoryAnimation from '../Animation';
import ListStyles from './List.styles';
import StoryImage from '../Image';
Expand All @@ -13,7 +15,7 @@ import StoryFooter from '../Footer';
const StoryList: FC<StoryListProps> = ( {
id, stories, index, x, activeUser, activeStory, progress, seenStories, paused,
onLoad, videoProps, progressColor, progressActiveColor, mediaContainerStyle, imageStyles,
imageProps, progressContainerStyle, imageOverlayView, ...props
imageProps, progressContainerStyle, imageOverlayView, hideElements, videoDuration, ...props
} ) => {

const imageHeight = useSharedValue( HEIGHT );
Expand All @@ -24,6 +26,10 @@ const StoryList: FC<StoryListProps> = ( {
);

const animatedStyles = useAnimatedStyle( () => ( { height: imageHeight.value } ) );
const contentStyles = useAnimatedStyle( () => ( {
opacity: withTiming( hideElements.value ? 0 : 1 ),
...ListStyles.content,
} ) );

const onImageLayout = ( height: number ) => {

Expand All @@ -41,7 +47,7 @@ const StoryList: FC<StoryListProps> = ( {
<StoryImage
stories={stories}
activeStory={activeStory}
defaultImage={stories[lastSeenIndex + 1]?.sourceUrl ?? stories[0]?.sourceUrl}
defaultStory={stories[lastSeenIndex + 1] ?? stories[0]}
isDefaultVideo={( stories[lastSeenIndex + 1]?.mediaType ?? stories[0]?.mediaType ) === 'video'}
onImageLayout={onImageLayout}
onLoad={onLoad}
Expand All @@ -51,19 +57,22 @@ const StoryList: FC<StoryListProps> = ( {
mediaContainerStyle={mediaContainerStyle}
imageStyles={imageStyles}
imageProps={imageProps}
videoDuration={videoDuration}
/>
{imageOverlayView}
<Progress
active={isActive}
activeStory={activeStoryIndex}
progress={progress}
length={stories.length}
progressColor={progressColor}
progressActiveColor={progressActiveColor}
progressContainerStyle={progressContainerStyle}
/>
<StoryHeader {...props} />
<StoryContent stories={stories} active={isActive} activeStory={activeStory} />
<Animated.View style={contentStyles}>
{imageOverlayView}
<Progress
active={isActive}
activeStory={activeStoryIndex}
progress={progress}
length={stories.length}
progressColor={progressColor}
progressActiveColor={progressActiveColor}
progressContainerStyle={progressContainerStyle}
/>
<StoryHeader {...props} />
<StoryContent stories={stories} active={isActive} activeStory={activeStory} />
</Animated.View>
</Animated.View>
<StoryFooter stories={stories} active={isActive} activeStory={activeStory} />
</StoryAnimation>
Expand Down
38 changes: 31 additions & 7 deletions src/components/Modal/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import Animated, {
useDerivedValue, useSharedValue, withTiming,
} from 'react-native-reanimated';
import {
ANIMATION_CONFIG, HEIGHT, LONG_PRESS_DURATION, WIDTH,
HEIGHT, LONG_PRESS_DURATION, STORY_ANIMATION_DURATION, WIDTH,
} from '../../core/constants';
import { GestureContext, StoryModalProps, StoryModalPublicMethods } from '../../core/dto/componentsDTO';
import GestureHandler from './gesture';
Expand All @@ -17,7 +17,9 @@ import ModalStyles from './Modal.styles';

const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {
stories, seenStories, duration, videoDuration, storyAvatarSize, textStyle, containerStyle,
backgroundColor, videoProps, closeIconColor, modalAnimationDuration = 800, onLoad, onShow, onHide,
backgroundColor, videoProps, closeIconColor, modalAnimationDuration = STORY_ANIMATION_DURATION,
storyAnimationDuration = STORY_ANIMATION_DURATION, hideElementsOnLongPress,
onLoad, onShow, onHide,
onSeenStoriesChange, onSwipeUp, onStoryStart, onStoryEnd, ...props
}, ref ) => {

Expand All @@ -29,6 +31,8 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {
const currentStory = useSharedValue( stories[0]?.stories[0]?.id );
const paused = useSharedValue( false );
const durationValue = useSharedValue( duration );
const isLongPress = useSharedValue( false );
const hideElements = useSharedValue( false );

const userIndex = useDerivedValue( () => Math.round( x.value / WIDTH ) );
const storyIndex = useDerivedValue( () => stories[userIndex.value]?.stories.findIndex(
Expand Down Expand Up @@ -59,6 +63,7 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {
{ duration: modalAnimationDuration },
() => runOnJS( setVisible )( false ),
);
cancelAnimation( animation );

};

Expand Down Expand Up @@ -116,7 +121,7 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {
const newUserIndex = stories.findIndex( ( story ) => story.id === id );
const newX = newUserIndex * WIDTH;

x.value = animated ? withTiming( newX, ANIMATION_CONFIG ) : newX;
x.value = animated ? withTiming( newX, { duration: storyAnimationDuration } ) : newX;

if ( sameUser ) {

Expand All @@ -126,7 +131,7 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {

}

if ( onStoryEnd ) {
if ( onStoryEnd && animated ) {

runOnJS( onStoryEnd )( previousUser ?? userId.value, currentStory.value );

Expand Down Expand Up @@ -327,10 +332,27 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {

};

const onLongPress = () => startAnimation( true );
const onLongPress = () => {

isLongPress.value = true;
hideElements.value = hideElementsOnLongPress ?? false;

};

const onPress = ( { nativeEvent: { locationX } }: GestureResponderEvent ) => {

hideElements.value = false;

if ( isLongPress.value ) {

isLongPress.value = false;
paused.value = false;
startAnimation( true );

return;

}

if ( locationX < WIDTH / 2 ) {

const success = toPreviousStory();
Expand Down Expand Up @@ -404,7 +426,7 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {
<Animated.View style={ModalStyles.container} testID="storyModal">
<Pressable
onPressIn={onPressIn}
onPress={onPress}
onPressOut={onPress}
onLongPress={onLongPress}
delayLongPress={LONG_PRESS_DURATION}
style={ModalStyles.container}
Expand All @@ -426,7 +448,7 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {
onLoad?.();
startAnimation(
undefined,
value !== undefined ? ( videoDuration ?? value ) : duration,
value !== undefined ? value : duration,
);

}}
Expand All @@ -435,6 +457,8 @@ const StoryModal = forwardRef<StoryModalPublicMethods, StoryModalProps>( ( {
paused={paused}
videoProps={videoProps}
closeColor={closeIconColor}
hideElements={hideElements}
videoDuration={videoDuration}
key={story.id}
{...props}
/>
Expand Down
2 changes: 1 addition & 1 deletion src/core/constants/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,6 @@ export const AVATAR_SIZE = 60;
export const AVATAR_OFFSET = 5;
export const STORY_AVATAR_SIZE = 26;

export const ANIMATION_CONFIG = { duration: 800 };
export const STORY_ANIMATION_DURATION = 800;
export const ANIMATION_DURATION = 10000;
export const LONG_PRESS_DURATION = 500;
Loading
Loading