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',