Skip to content

Commit

Permalink
Merge pull request #274 from slytter/master
Browse files Browse the repository at this point in the history
Add preloading to stories
  • Loading branch information
mohitk05 committed Jul 13, 2023
2 parents ed0b93b + 49c1c28 commit d8deeb6
Show file tree
Hide file tree
Showing 8 changed files with 114 additions and 8 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,5 @@
node_modules
dist
stats.json
report.html
report.html
.idea
4 changes: 3 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -70,20 +70,22 @@ Here `stories` is an array of story objects, which can be of various types as de
| `onPrevious` | Function | - | Callback when the user taps/press to go back to the previous story |
| `keyboardNavigation` | Boolean | false | Attaches arrow key listeners to navigate between stories if true. Also adds up arrow key listener for opening See More and Escape/down arrow for closing it |
| `preventDefault` | Boolean | false | Disable the default behavior when user click the component |
| `preloadCount` | number | 1 | Determines how many stories should be preloaded ahead of the current story index. |

### Story object

Instead of simple string url, a comprehensive 'story object' can also be passed in the `stories` array.

| Property | Description |
| ------------------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
|--------------------|-----------------------------------------------------------------------------------------------------------------------------------------------|
| `url` | The url of the resource, be it image or video. |
| `type` | Optional. Type of the story. `type: 'video' | 'image'`. Type `video` is necessary for a video story. |
| `duration` | Optional. Duration for which a story should persist. |
| `header` | Optional. Adds a header on the top. Object with `heading`, `subheading` and `profileImage` properties. |
| `seeMore` | Optional. Adds a see more icon at the bottom of the story. On clicking, opens up this component. (v2: updated to Function instead of element) |
| `seeMoreCollapsed` | Optional. Send custom component to be rendered instead of the default 'See More' text. | |
| `styles` | Optional. Override the default story styles mentioned below. |
| `preloadResource` | Optional. Whether to preload the resource or not, defaults to `true` for images and `false` for videos (video preloading is experimental) |

### Default story styles

Expand Down
4 changes: 3 additions & 1 deletion example/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

22 changes: 22 additions & 0 deletions example/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,7 @@ function App() {
<div className="stories">
<Suspense>
<StoriesLazy
preloadCount={3}
loop
keyboardNavigation
defaultInterval={8000}
Expand Down Expand Up @@ -322,6 +323,27 @@ const stories2 = [
{
content: Story2,
},
{
url: "https://plus.unsplash.com/premium_photo-1676231417481-5eae894e7f68?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=1742&q=80",
},
{
url: "https://images.unsplash.com/photo-1676321626679-2513969695d3?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
},
{
url: "https://images.unsplash.com/photo-1676359912443-1bf438548584?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=870&q=80",
},
{
url: "https://images.unsplash.com/photo-1676316698468-a907099ad5bc?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=928&q=80",
preloadResource: false,
},
{
url: "https://images.unsplash.com/photo-1676310483825-daa08914445e?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=2920&q=80",
preloadResource: false,
},
{
url: "https://images.unsplash.com/photo-1676321685222-0b527e61d5c7?ixlib=rb-4.0.3&ixid=MnwxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8&auto=format&fit=crop&w=774&q=80",
preloadResource: false,
},
];

const image = {
Expand Down
7 changes: 6 additions & 1 deletion src/components/Container.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import {
StoriesContext as StoriesContextInterface,
} from "./../interfaces";
import useIsMounted from "./../util/use-is-mounted";
import { usePreLoader } from "../util/usePreLoader";

export default function () {
const [currentId, setCurrentId] = useState<number>(0);
Expand All @@ -30,10 +31,13 @@ export default function () {
storyContainerStyles = {},
onAllStoriesEnd,
onPrevious,
onNext
onNext,
preloadCount,
} = useContext<GlobalCtx>(GlobalContext);
const { stories } = useContext<StoriesContextInterface>(StoriesContext);

usePreLoader(stories, currentId, preloadCount);

useEffect(() => {
if (typeof currentIndex === "number") {
if (currentIndex >= 0 && currentIndex < stories.length) {
Expand All @@ -47,6 +51,7 @@ export default function () {
}
}, [currentIndex]);


useEffect(() => {
if (typeof isPaused === "boolean") {
setPause(isPaused);
Expand Down
10 changes: 7 additions & 3 deletions src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,9 +31,12 @@ const ReactInstaStories = function (props: ReactInstaStoriesProps) {
onNext: props.onNext,
onPrevious: props.onPrevious,
keyboardNavigation: props.keyboardNavigation,
preventDefault: props.preventDefault
preventDefault: props.preventDefault,
preloadCount: props.preloadCount,
}
const [stories, setStories] = useState<{ stories: Story[] }>({ stories: generateStories(props.stories, renderers) });


useEffect(() => {
setStories({ stories: generateStories(props.stories, renderers) });
}, [props.stories, props.renderers]);
Expand Down Expand Up @@ -66,10 +69,11 @@ const generateStories = (stories: Story[], renderers: { renderer: Renderer, test
ReactInstaStories.defaultProps = {
width: 360,
height: 640,
defaultInterval: 4000
defaultInterval: 4000,
preloadCount: 1,
}

export const WithHeader = withHeader;
export const WithSeeMore = withSeeMore;

export default ReactInstaStories
export default ReactInstaStories
6 changes: 5 additions & 1 deletion src/interfaces.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ export interface ReactInstaStoriesProps {
onPrevious?: Function;
keyboardNavigation?: boolean;
preventDefault?: boolean;
preloadCount?: number;
}

export interface GlobalCtx {
Expand Down Expand Up @@ -55,6 +56,7 @@ export interface GlobalCtx {
onNext?: Function;
keyboardNavigation?: boolean;
preventDefault?: boolean;
preloadCount?: number;
}

type NumberOrString = number | string;
Expand Down Expand Up @@ -115,7 +117,9 @@ export interface Story {
duration?: number;
styles?: object;
content?: Renderer;
originalContent?: Renderer;
originalContent?: Renderer
// Whether to preload the resource or not, defaults to `true` for images and `false` for videos (video preloading is experimental)
preloadResource?: boolean;
}

export interface Header {
Expand Down
66 changes: 66 additions & 0 deletions src/util/usePreLoader.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
import {Story} from "../interfaces";
import {useEffect} from "react";


// Caches given Story[] using HTMLImageElement and HTMLVideoElement
const cacheContent = async (contents: Story[]) => {
const promises = contents.map((content) => {
return new Promise(function (resolve, reject) {
if(!content.url) return

if(content.type === 'video') {
const video = document.createElement('video');
video.src = content.url;
video.onloadeddata = resolve;
video.onerror = reject;
return;
}

const img = new Image();
img.src = content.url;
img.onload = resolve;
img.onerror = reject;

})
})

await Promise.all(promises);
}

// Keeps track of urls that have been loaded
const urlsLoaded = new Set<string>();

// Pushes urls to urlsLoaded
const markUrlsLoaded = (contents: Story[]) => {
contents.forEach((content) => {
urlsLoaded.add(content.url)
})
}


// Returns true if given Story should be preloaded
const shouldPreload = (content: Story) => {
if (!content.url) return false
if (urlsLoaded.has(content.url)) return false
if (content.preloadResource !== undefined) return content.preloadResource
if (content.type === 'video') return false

return true
}

// Preloads images and videos from given Story[] using a cursor and preloadCount
// Preload count is the number of images/videos to preload after the cursor
// Cursor is the current index to start preloading from
export const usePreLoader = (contents: Story[], cursor: number, preloadCount: number) => {
useEffect(() => {
const start = cursor + 1;
const end = cursor + preloadCount + 1;

const toPreload = contents
.slice(start, end)
.filter(shouldPreload);

markUrlsLoaded(toPreload)
cacheContent(toPreload)
}, [cursor, preloadCount, contents])
}

1 comment on commit d8deeb6

@vercel
Copy link

@vercel vercel bot commented on d8deeb6 Jul 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please sign in to comment.