From 2412027f07bdcb44557b14bd02a397c9a7150d16 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?R=C4=B1dvan=20Altun?= Date: Wed, 9 Aug 2023 14:33:28 +0300 Subject: [PATCH] feat: ability to prefetch story images --- README.md | 41 ++++++++++--------- example/src/App.tsx | 3 ++ src/components/Story.tsx | 39 ++++++++++++++++++ src/components/StoryCircleListItem.tsx | 2 +- src/components/StoryListItem.tsx | 2 +- src/helpers/useMountEffect.ts | 15 +++++++ .../{StateHelpers.ts => usePrevious.ts} | 4 +- 7 files changed, 83 insertions(+), 23 deletions(-) create mode 100644 src/helpers/useMountEffect.ts rename src/helpers/{StateHelpers.ts => usePrevious.ts} (73%) diff --git a/README.md b/README.md index e12722d..842a03a 100644 --- a/README.md +++ b/README.md @@ -29,26 +29,27 @@ npm i react-native-story-component ## Props -| Name | Description | Type | Default Value | -| :------------------- | :--------------------------------------- | :---------------------------------------------- | :-----------: | -| data | Array of stories. | UserStory[] | | -| unPressedBorderColor | Unpressed border color of profile circle | color | red | -| pressedBorderColor | Pressed border color of profile circle | color | grey | -| onClose | Todo when close | (item: UserStory) => void | null | -| onStart | Todo when start | (item: UserStory) => void | null | -| duration | Per story duration in seconds | number | 10 | -| swipeText | Text of swipe component | string | Swipe Up | -| customSwipeUpButton | Custom component for swipe area | () => ReactNode | | -| customCloseButton | Custom component for close button | () => ReactNode | | -| customStoryList | Custom component for story list | (props: CustomStoryList) => React.ReactNode | | -| customStoryView | Custom component for story view | (props: CustomStoryView) => React.ReactNode | | -| customProfileBanner | Custom component for profile banner | (props: CustomProfileBanner) => React.ReactNode | | -| customStoryImage | Custom component for story image | (props: CustomStoryImage) => React.ReactNode | | -| avatarSize | Size of avatar circle | number | 60 | -| showAvatarText | Show or hide avatar text | bool | true | -| showProfileBanner | Show or hide profile banner | bool | true | -| textStyle | Avatar text style | TextStyle | | -| storyListStyle | Story list view style | ViewStyle | | +| Name | Description | Type | Default Value | +| :------------------- | :---------------------------------------- | :---------------------------------------------- | :-----------: | +| data | Array of stories. | UserStory[] | | +| unPressedBorderColor | Unpressed border color of profile circle | color | red | +| pressedBorderColor | Pressed border color of profile circle | color | grey | +| onClose | Todo when close | (item: UserStory) => void | null | +| onStart | Todo when start | (item: UserStory) => void | null | +| duration | Per story duration in seconds | number | 10 | +| swipeText | Text of swipe component | string | Swipe Up | +| customSwipeUpButton | Custom component for swipe area | () => ReactNode | | +| customCloseButton | Custom component for close button | () => ReactNode | | +| customStoryList | Custom component for story list | (props: CustomStoryList) => React.ReactNode | | +| customStoryView | Custom component for story view | (props: CustomStoryView) => React.ReactNode | | +| customProfileBanner | Custom component for profile banner | (props: CustomProfileBanner) => React.ReactNode | | +| customStoryImage | Custom component for story image | (props: CustomStoryImage) => React.ReactNode | | +| avatarSize | Size of avatar circle | number | 60 | +| showAvatarText | Show or hide avatar text | bool | true | +| showProfileBanner | Show or hide profile banner | bool | true | +| textStyle | Avatar text style | TextStyle | | +| prefetchImages | Prefetch story images | bool | true | +| onImagesPrefetched | Callback function for prefetching process | (allImagesPrefetched: bool) => void | | ## Usage diff --git a/example/src/App.tsx b/example/src/App.tsx index 2be095e..522b45e 100644 --- a/example/src/App.tsx +++ b/example/src/App.tsx @@ -67,6 +67,9 @@ const App = () => { // // ); // }} + onImagesPrefetched={(status) => { + console.log('is all images prefetched ->', status); + }} customSwipeUpButton={CustomSwipeButton} /> diff --git a/src/components/Story.tsx b/src/components/Story.tsx index aa0c938..d8cbb61 100644 --- a/src/components/Story.tsx +++ b/src/components/Story.tsx @@ -5,6 +5,7 @@ import { Platform, StatusBar, StyleSheet, + Image, } from 'react-native'; import Modal from 'react-native-modalbox'; @@ -15,6 +16,7 @@ import AndroidCubeEffect from '../animations/AndroidCubeEffect'; import CubeNavigationHorizontal from '../animations/CubeNavigationHorizontal'; import { isNullOrWhitespace, isUrl } from '../helpers/ValidationHelpers'; +import useMountEffect from '../helpers/useMountEffect'; import { ActionStates } from '../index'; import type { @@ -46,6 +48,8 @@ interface StoryProps { showAvatarText?: boolean; showProfileBanner?: boolean; avatarTextStyle?: TextStyle; + prefetchImages?: boolean; + onImagesPrefetched?: (allImagesPrefethed: boolean) => void; } const Story = (props: StoryProps) => { @@ -67,6 +71,8 @@ const Story = (props: StoryProps) => { showAvatarText, showProfileBanner, avatarTextStyle, + prefetchImages, + onImagesPrefetched, } = props; const cubeRef = useRef(null); @@ -101,6 +107,38 @@ const Story = (props: StoryProps) => { } }, [currentPage, dataState, selectedData]); + useMountEffect(() => { + if (prefetchImages) { + let preFetchTasks: Promise[] = []; + const images = data.flatMap((story) => { + const storyImages = story.stories.map((storyItem) => { + return storyItem.image; + }); + + return storyImages; + }); + + images.forEach((image) => { + preFetchTasks.push(Image.prefetch(image)); + }); + + Promise.all(preFetchTasks).then((results) => { + let downloadedAll = true; + + results.forEach((result) => { + if (!result) { + //error occurred downloading a pic + downloadedAll = false; + } + }); + + if (onImagesPrefetched) { + onImagesPrefetched(downloadedAll); + } + }); + } + }); + useEffect(() => { handleSeen(); }, [currentPage, handleSeen]); @@ -249,6 +287,7 @@ const Story = (props: StoryProps) => { Story.defaultProps = { showAvatarText: true, showProfileBanner: true, + prefetchImages: true, }; const styles = StyleSheet.create({ diff --git a/src/components/StoryCircleListItem.tsx b/src/components/StoryCircleListItem.tsx index f90178f..ff7628e 100644 --- a/src/components/StoryCircleListItem.tsx +++ b/src/components/StoryCircleListItem.tsx @@ -10,7 +10,7 @@ import { } from 'react-native'; import { isUrl } from '../helpers/ValidationHelpers'; -import { usePrevious } from '../helpers/StateHelpers'; +import usePrevious from '../helpers/usePrevious'; import type { TextStyle } from 'react-native'; import type { UserStory } from '../index'; diff --git a/src/components/StoryListItem.tsx b/src/components/StoryListItem.tsx index 3b4f737..5031feb 100644 --- a/src/components/StoryListItem.tsx +++ b/src/components/StoryListItem.tsx @@ -21,7 +21,7 @@ import { } from 'react-native'; import GestureRecognizer from 'react-native-swipe-gestures'; -import { usePrevious } from '../helpers/StateHelpers'; +import usePrevious from '../helpers/usePrevious'; import { isNullOrWhitespace } from '../helpers/ValidationHelpers'; import { ActionStates } from '../index'; diff --git a/src/helpers/useMountEffect.ts b/src/helpers/useMountEffect.ts new file mode 100644 index 0000000..3a81201 --- /dev/null +++ b/src/helpers/useMountEffect.ts @@ -0,0 +1,15 @@ +import { useEffect } from 'react'; + +const useMountEffect = (effect: () => void) => { + if (typeof effect !== 'function') { + console.error('Effect must be a function'); + } + + useEffect(() => { + effect?.(); + + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); +}; + +export default useMountEffect; diff --git a/src/helpers/StateHelpers.ts b/src/helpers/usePrevious.ts similarity index 73% rename from src/helpers/StateHelpers.ts rename to src/helpers/usePrevious.ts index 46160dd..45f52f1 100644 --- a/src/helpers/StateHelpers.ts +++ b/src/helpers/usePrevious.ts @@ -2,10 +2,12 @@ import { useEffect, useRef } from 'react'; // @see: https://usehooks.com/usePrevious/ -export const usePrevious = (value: any) => { +const usePrevious = (value: any) => { const ref = useRef(); useEffect(() => { ref.current = value; }); return ref.current; }; + +export default usePrevious;