diff --git a/CHANGELOG.md b/CHANGELOG.md
index 1d8cbbd..7bf1a21 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,12 @@
All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines.
+### 1.3.8 (2024-10-12)
+
+### 1.3.7 (2024-10-08)
+
+### 1.3.6 (2024-10-08)
+
### 1.3.5 (2024-08-10)
### 1.3.4 (2024-07-30)
diff --git a/README.md b/README.md
index d4f41af..3c7b034 100644
--- a/README.md
+++ b/README.md
@@ -38,10 +38,12 @@ npm install react-native-gesture-handler
npm install @birdwingo/react-native-instagram-stories
```
-## Integration with Storage and Video
+## Integration with Storage, Flashlist and Video
The component offers an option to save and track the progress of seen stories using `saveProgress`. If you use `saveProgress`, please make sure you have `@react-native-async-storage/async-storage` installed.
+If you have installed Flashlist, it will be automatically used for avatars list.
+
If you use video in your stories, please make sure you have `react-native-video` installed.
## Usage
@@ -97,7 +99,7 @@ export default YourComponent;
`avatarSeenBorderColors` | string[] | [ '#2A2A2C' ] | An array of string colors representing the border colors of seen story avatars.
`avatarSize` | number | 60 | The size of the story avatars.
`storyAvatarSize` | number | 25 | The size of the avatars shown in the header of each story.
- `avatarListContainerStyle` | ScrollViewProps['contentContainerStyle'] | | Additional styles for the avatar scroll list container.
+ `avatarListContainerStyle` | ScrollViewProps['contentContainerStyle'], FlashListProps | | Additional styles for the avatar scroll list container.
`avatarListContainerProps` | ScrollViewProps | | Props to be passed to the avatar list ScrollView component.
`containerStyle` | ViewStyle | | Additional styles for the story container.
`textStyle` | TextStyle | | Additional styles for text elements.
@@ -122,7 +124,7 @@ export default YourComponent;
`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
-| `hideOverlayOnLongPress` | `boolean` | (Optional) Controls whether the image overlay hides when `hideElementOnLongPress` is set to `true`. If `true`, the overlay will hide along with other elements on long press. If `false`, only the other elements (e.g., header, progress bar) will hide, and the overlay will remain visible. Default is the value of `hideElementOnLongPress`. |
+ `hideOverlayOnLongPress` | `boolean` | The value of `hideElementOnLongPress` | Controls whether the image overlay hides when `hideElementOnLongPress` is set to `true`. If `true`, the overlay will hide along with other elements on long press. If `false`, only the other elements (e.g., header, progress bar) will hide, and the overlay will remain visible.
`loopingStories` | `'none'` | `'onlyLast'` | `'all'` | `'none'` | A string indicating whether to continue stories after last story was shown. If set to `'none'` modal will be closed after all stories were played, if set to `'onlyLast'` stories will loop on last user only after all stories were played. If set to `'all'` stories will play from beginning after all stories were played.
`statusBarTranslucent` | boolean | false | A property passed to React native Modal component
`footerComponent` | ReactNode | | A custom component, such as a floating element, that can be added to the modal.
@@ -148,6 +150,7 @@ export default YourComponent;
`goToPreviousStory` | () => void | Goes to previous story item
`goToNextStory` | () => void | Goes to next story item
`getCurrentStory` | () => {userId?: string, storyId?: string} | Returns current userId and storyId
+ `goToSpecificStory` | ( userId: string, index: number ) => void | Change current playing story to defined index if index not exist then start playing first story
## Types
diff --git a/jest.setup.js b/jest.setup.js
index f5c774c..365e936 100644
--- a/jest.setup.js
+++ b/jest.setup.js
@@ -128,3 +128,20 @@ jest.mock('./src/components/Image/video', () => {
return ;
};
});
+
+jest.mock('@shopify/flash-list', () => {
+
+ const React = require('react');
+ const { ScrollView } = require('react-native');
+
+ return {FlashList: ({ data, renderItem, ...props }) => {
+
+ return (
+
+ {data.map(( item, index ) => renderItem({ item, index }))}
+
+ )
+
+ }};
+
+});
\ No newline at end of file
diff --git a/package-lock.json b/package-lock.json
index 14105d3..952aa1d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@birdwingo/react-native-instagram-stories",
- "version": "1.3.5",
+ "version": "1.3.8",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@birdwingo/react-native-instagram-stories",
- "version": "1.3.5",
+ "version": "1.3.8",
"license": "MIT",
"devDependencies": {
"@babel/preset-env": "^7.22.9",
@@ -14,6 +14,7 @@
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"@react-native-async-storage/async-storage": "^1.19.2",
+ "@shopify/flash-list": "^1.7.1",
"@testing-library/jest-native": "^5.4.2",
"@testing-library/react-native": "^12.1.3",
"@tsconfig/react-native": "^3.0.0",
@@ -4855,6 +4856,21 @@
"react-native": "*"
}
},
+ "node_modules/@shopify/flash-list": {
+ "version": "1.7.1",
+ "resolved": "https://registry.npmjs.org/@shopify/flash-list/-/flash-list-1.7.1.tgz",
+ "integrity": "sha512-sUYl7h8ydJutufA26E42Hj7cLvaBTpkMIyNJiFrxUspkcANb6jnFiLt9rEwAuDjvGk/C0lHau+WyT6ZOxqVPwg==",
+ "dev": true,
+ "dependencies": {
+ "recyclerlistview": "4.2.1",
+ "tslib": "2.6.3"
+ },
+ "peerDependencies": {
+ "@babel/runtime": "*",
+ "react": "*",
+ "react-native": "*"
+ }
+ },
"node_modules/@sideway/address": {
"version": "4.1.4",
"resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.4.tgz",
@@ -13692,11 +13708,11 @@
}
},
"node_modules/micromatch": {
- "version": "4.0.5",
- "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.5.tgz",
- "integrity": "sha512-DMy+ERcEW2q8Z2Po+WNXuw3c5YaUSFjAO5GsJqfEl7UjvtIuFKO6ZrKvcItdy98dwFI2N1tg3zNIdKaQT+aNdA==",
+ "version": "4.0.8",
+ "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz",
+ "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==",
"dependencies": {
- "braces": "^3.0.2",
+ "braces": "^3.0.3",
"picomatch": "^2.3.1"
},
"engines": {
@@ -15167,6 +15183,21 @@
"node": ">= 4"
}
},
+ "node_modules/recyclerlistview": {
+ "version": "4.2.1",
+ "resolved": "https://registry.npmjs.org/recyclerlistview/-/recyclerlistview-4.2.1.tgz",
+ "integrity": "sha512-NtVYjofwgUCt1rEsTp6jHQg/47TWjnO92TU2kTVgJ9wsc/ely4HnizHHa+f/dI7qaw4+zcSogElrLjhMltN2/g==",
+ "dev": true,
+ "dependencies": {
+ "lodash.debounce": "4.0.8",
+ "prop-types": "15.8.1",
+ "ts-object-utils": "0.0.5"
+ },
+ "peerDependencies": {
+ "react": ">= 15.2.1",
+ "react-native": ">= 0.30.0"
+ }
+ },
"node_modules/redent": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/redent/-/redent-3.0.0.tgz",
@@ -15478,9 +15509,9 @@
}
},
"node_modules/send": {
- "version": "0.18.0",
- "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz",
- "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==",
+ "version": "0.19.0",
+ "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz",
+ "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==",
"peer": true,
"dependencies": {
"debug": "2.6.9",
@@ -15565,20 +15596,29 @@
}
},
"node_modules/serve-static": {
- "version": "1.15.0",
- "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
- "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==",
+ "version": "1.16.2",
+ "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz",
+ "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==",
"peer": true,
"dependencies": {
- "encodeurl": "~1.0.2",
+ "encodeurl": "~2.0.0",
"escape-html": "~1.0.3",
"parseurl": "~1.3.3",
- "send": "0.18.0"
+ "send": "0.19.0"
},
"engines": {
"node": ">= 0.8.0"
}
},
+ "node_modules/serve-static/node_modules/encodeurl": {
+ "version": "2.0.0",
+ "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz",
+ "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==",
+ "peer": true,
+ "engines": {
+ "node": ">= 0.8"
+ }
+ },
"node_modules/set-blocking": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz",
@@ -16345,6 +16385,12 @@
}
}
},
+ "node_modules/ts-object-utils": {
+ "version": "0.0.5",
+ "resolved": "https://registry.npmjs.org/ts-object-utils/-/ts-object-utils-0.0.5.tgz",
+ "integrity": "sha512-iV0GvHqOmilbIKJsfyfJY9/dNHCs969z3so90dQWsO1eMMozvTpnB1MEaUbb3FYtZTGjv5sIy/xmslEz0Rg2TA==",
+ "dev": true
+ },
"node_modules/tsconfig-paths": {
"version": "3.14.2",
"resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.14.2.tgz",
@@ -16382,10 +16428,9 @@
}
},
"node_modules/tslib": {
- "version": "2.6.1",
- "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.1.tgz",
- "integrity": "sha512-t0hLfiEKfMUoqhG+U1oid7Pva4bbDPHYfJNiB7BiIjRkj1pyC++4N3huJfqY6aRH6VTB0rvtzQwjM4K6qpfOig==",
- "peer": true
+ "version": "2.6.3",
+ "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
+ "integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ=="
},
"node_modules/tsutils": {
"version": "3.21.0",
diff --git a/package.json b/package.json
index 20a77c6..266c5c0 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@birdwingo/react-native-instagram-stories",
- "version": "1.3.5",
+ "version": "1.3.8",
"description": "A versatile and captivating React Native component that empowers developers to seamlessly integrate Instagram-style stories into their mobile applications, fostering an engaging and interactive user experience.",
"main": "src/index.tsx",
"source": "src/index.tsx",
@@ -45,6 +45,7 @@
"@commitlint/cli": "^17.6.7",
"@commitlint/config-conventional": "^17.6.7",
"@react-native-async-storage/async-storage": "^1.19.2",
+ "@shopify/flash-list": "^1.7.1",
"@testing-library/jest-native": "^5.4.2",
"@testing-library/react-native": "^12.1.3",
"@tsconfig/react-native": "^3.0.0",
diff --git a/src/components/AvatarList/index.tsx b/src/components/AvatarList/index.tsx
new file mode 100644
index 0000000..1dad709
--- /dev/null
+++ b/src/components/AvatarList/index.tsx
@@ -0,0 +1,68 @@
+import React, { FC, memo } from 'react';
+import { ScrollView } from 'react-native';
+import StoryAvatar from '../Avatar';
+import { StoryAvatarListProps } from '~/core/dto/componentsDTO';
+import { InstagramStoryProps } from '~/core/dto/instagramStoriesDTO';
+
+let FlashList: any;
+
+try {
+
+ // eslint-disable-next-line global-require
+ FlashList = require( '@shopify/flash-list' ).FlashList;
+
+} catch ( error ) {
+
+ FlashList = null;
+
+}
+
+const StoryAvatarList: FC = ( {
+ stories, loadingStory, seenStories, colors, seenColors, size,
+ showName, nameTextStyle, nameTextProps, listContainerProps, listContainerStyle,
+ avatarListContainerProps, avatarListContainerStyle, onPress,
+} ) => {
+
+ const renderItem = ( story: InstagramStoryProps ) => story.renderAvatar?.()
+ ?? ( ( story.avatarSource || story.imgUrl ) && (
+ onPress( story.id )}
+ colors={colors}
+ seenColors={seenColors}
+ size={size}
+ showName={showName}
+ nameTextStyle={nameTextStyle}
+ nameTextProps={nameTextProps}
+ key={`avatar${story.id}`}
+ />
+ ) );
+
+ if ( FlashList ) {
+
+ return (
+ renderItem( item )}
+ keyExtractor={( item: InstagramStoryProps ) => item.id}
+ contentContainerStyle={[ listContainerStyle, avatarListContainerStyle ]}
+ testID="storiesList"
+ />
+ );
+
+ }
+
+ return (
+
+ {stories.map( renderItem )}
+
+ );
+
+};
+
+export default memo( StoryAvatarList );
diff --git a/src/components/InstagramStories/index.tsx b/src/components/InstagramStories/index.tsx
index ab743de..ee88529 100644
--- a/src/components/InstagramStories/index.tsx
+++ b/src/components/InstagramStories/index.tsx
@@ -2,8 +2,7 @@ import React, {
forwardRef, useImperativeHandle, useState, useEffect, useRef, memo,
} from 'react';
import { useSharedValue } from 'react-native-reanimated';
-import { Image, ScrollView } from 'react-native';
-import StoryAvatar from '../Avatar';
+import { Image } from 'react-native';
import { clearProgressStorage, getProgressStorage, setProgressStorage } from '../../core/helpers/storage';
import { InstagramStoriesProps, InstagramStoriesPublicMethods } from '../../core/dto/instagramStoriesDTO';
import { ProgressStorageProps } from '../../core/dto/helpersDTO';
@@ -13,6 +12,7 @@ import {
} from '../../core/constants';
import StoryModal from '../Modal';
import { StoryModalPublicMethods } from '../../core/dto/componentsDTO';
+import StoryAvatarList from '../AvatarList';
const InstagramStories = forwardRef( ( {
stories,
@@ -175,6 +175,7 @@ const InstagramStories = forwardRef modalRef.current?.goToSpecificStory( userId, index ),
hide: () => modalRef.current?.hide(),
show: ( id ) => {
@@ -227,24 +228,22 @@ const InstagramStories = forwardRef
{!hideAvatarList && (
-
- {data.map( ( story ) => story.renderAvatar?.()
- ?? ( ( story.avatarSource || story.imgUrl ) && (
- onPress( story.id )}
- colors={avatarBorderColors}
- seenColors={avatarSeenBorderColors}
- size={avatarSize}
- showName={showName}
- nameTextStyle={nameTextStyle}
- nameTextProps={nameTextProps}
- key={`avatar${story.id}`}
- />
- ) ) )}
-
+
)}
= ( {
id, stories, index, x, activeUser, activeStory, progress, seenStories, paused,
onLoad, videoProps, progressColor, progressActiveColor, mediaContainerStyle, imageStyles,
- imageProps, progressContainerStyle, imageOverlayView, hideElements,hideOverlayViewOnLongPress, videoDuration, ...props
+ imageProps, progressContainerStyle, imageOverlayView, hideElements, hideOverlayViewOnLongPress,
+ videoDuration, ...props
} ) => {
const imageHeight = useSharedValue( HEIGHT );
@@ -58,21 +59,27 @@ const StoryList: FC = ( {
imageProps={imageProps}
videoDuration={videoDuration}
/>
-
+
{imageOverlayView}
-
-
-
-
-
+
+
+
+
+
diff --git a/src/components/Modal/index.tsx b/src/components/Modal/index.tsx
index 4ce9fa8..06c18fd 100644
--- a/src/components/Modal/index.tsx
+++ b/src/components/Modal/index.tsx
@@ -423,6 +423,7 @@ const StoryModal = forwardRef( ( {
getCurrentStory: () => ( { userId: userId.value, storyId: currentStory.value } ),
goToPreviousStory: toPreviousStory,
goToNextStory: toNextStory,
+ goToSpecificStory: ( newUserId, index ) => scrollTo( newUserId, true, false, undefined, index ),
} ), [ userId.value, currentStory.value ] );
useEffect( () => {
diff --git a/src/core/constants/index.ts b/src/core/constants/index.ts
index 4ea5850..d36284d 100644
--- a/src/core/constants/index.ts
+++ b/src/core/constants/index.ts
@@ -1,6 +1,6 @@
import { Dimensions } from 'react-native';
-export const { width: WIDTH, height: HEIGHT } = Dimensions.get( 'window' );
+export const { width: WIDTH, height: HEIGHT } = Dimensions.get( 'screen' );
export const STORAGE_KEY = '@birdwingo/react-native-instagram-stories';
diff --git a/src/core/dto/componentsDTO.ts b/src/core/dto/componentsDTO.ts
index e5171e2..1261aa2 100644
--- a/src/core/dto/componentsDTO.ts
+++ b/src/core/dto/componentsDTO.ts
@@ -3,9 +3,26 @@ import {
ImageProps, ImageStyle, TextProps, TextStyle, ViewStyle,
} from 'react-native';
import { ReactNode } from 'react';
-import { InstagramStoryProps, StoryItemProps } from './instagramStoriesDTO';
+import { InstagramStoriesProps, InstagramStoryProps, StoryItemProps } from './instagramStoriesDTO';
import { ProgressStorageProps } from './helpersDTO';
+export interface StoryAvatarListProps {
+ stories: InstagramStoryProps[];
+ loadingStory: StoryAvatarProps['loadingStory'];
+ seenStories: StoryAvatarProps['seenStories'];
+ colors: StoryAvatarProps['colors'];
+ seenColors: StoryAvatarProps['seenColors'];
+ size: StoryAvatarProps['size'];
+ showName: InstagramStoriesProps['showName'];
+ nameTextStyle: InstagramStoriesProps['nameTextStyle'];
+ nameTextProps: InstagramStoriesProps['nameTextProps'];
+ listContainerStyle: InstagramStoriesProps['listContainerStyle'];
+ avatarListContainerStyle: InstagramStoriesProps['avatarListContainerStyle'];
+ listContainerProps: InstagramStoriesProps['listContainerProps'];
+ avatarListContainerProps: InstagramStoriesProps['avatarListContainerProps'];
+ onPress: ( id: string ) => void;
+}
+
export interface StoryAvatarProps extends InstagramStoryProps {
loadingStory: SharedValue;
seenStories: SharedValue;
@@ -44,6 +61,7 @@ export interface StoryModalProps {
imageProps?: ImageProps;
footerComponent?: ReactNode;
hideElementsOnLongPress?: boolean;
+ hideOverlayViewOnLongPress?: boolean;
loopingStories?: 'none' | 'all' | 'onlyLast';
statusBarTranslucent?: boolean;
onLoad: () => void;
@@ -63,6 +81,7 @@ export type StoryModalPublicMethods = {
goToPreviousStory: () => void;
goToNextStory: () => void;
getCurrentStory: () => { userId?: string, storyId?: string };
+ goToSpecificStory: ( userId: string, index?: number ) => void;
};
export type GestureContext = {
diff --git a/src/core/dto/instagramStoriesDTO.ts b/src/core/dto/instagramStoriesDTO.ts
index a37bf7e..a11f412 100644
--- a/src/core/dto/instagramStoriesDTO.ts
+++ b/src/core/dto/instagramStoriesDTO.ts
@@ -4,6 +4,7 @@ import {
ImageStyle,
ScrollViewProps, TextStyle, ViewStyle, TextProps,
} from 'react-native';
+import { FlashListProps } from '@shopify/flash-list';
export interface StoryItemProps {
id: string;
@@ -47,8 +48,8 @@ export interface InstagramStoriesProps {
/**
* @deprecated Use {@link avatarListContainerProps} instead.
*/
- listContainerProps?: ScrollViewProps;
- avatarListContainerProps?: ScrollViewProps;
+ listContainerProps?: ScrollViewProps | Partial>;
+ avatarListContainerProps?: ScrollViewProps | Partial>;
containerStyle?: ViewStyle;
textStyle?: TextStyle;
animationDuration?: number;
@@ -73,6 +74,7 @@ export interface InstagramStoriesProps {
hideAvatarList?: boolean;
imageOverlayView?: ReactNode;
hideElementsOnLongPress?: boolean;
+ hideOverlayViewOnLongPress?: boolean;
loopingStories?: 'none' | 'all' | 'onlyLast';
statusBarTranslucent?: boolean;
onShow?: ( id: string ) => void;
@@ -94,4 +96,5 @@ export type InstagramStoriesPublicMethods = {
goToPreviousStory: () => void;
goToNextStory: () => void;
getCurrentStory: () => { userId?: string, storyId?: string };
+ goToSpecificStory: ( userId: string, index?: number ) => void;
};
diff --git a/tests/index.test.js b/tests/index.test.js
index 96d8e8d..1a72502 100644
--- a/tests/index.test.js
+++ b/tests/index.test.js
@@ -31,7 +31,7 @@ jest.spyOn( Storage, 'setProgressStorage' ).mockImplementation( () => ( {} ) );
const stories = [ {
id: '1',
name: 'John Doe',
- imgUrl: 'https://picsum.photos/200/300',
+ avatarSource: 'https://picsum.photos/200/300',
stories: [ {
id: '1',
sourceUrl: 'https://picsum.photos/200/300',