fix: improve performance

This commit is contained in:
Wyatt Johnson
2020-08-11 13:26:51 -06:00
parent 8c6c8c1f72
commit 5acac741e4
2 changed files with 108 additions and 77 deletions
@@ -10,6 +10,7 @@ import { graphql } from "react-relay";
import { useCoralContext } from "coral-framework/lib/bootstrap";
import { useViewerEvent } from "coral-framework/lib/events";
import { IntersectionProvider } from "coral-framework/lib/intersection";
import { useLocal, withFragmentContainer } from "coral-framework/lib/relay";
import { GQLSTORY_MODE, GQLUSER_STATUS } from "coral-framework/schema";
import CLASSES from "coral-stream/classes";
@@ -228,10 +229,12 @@ export const StreamContainer: FunctionComponent<Props> = (props) => {
settings={props.settings}
/>
)}
<ViewersWatchingContainer
story={props.story}
settings={props.settings}
/>
<IntersectionProvider>
<ViewersWatchingContainer
story={props.story}
settings={props.settings}
/>
</IntersectionProvider>
<HorizontalGutter spacing={4} className={styles.tabBarContainer}>
<Flex
direction="row"
@@ -1,8 +1,14 @@
import { Localized } from "@fluent/react/compat";
import React, { FunctionComponent, useEffect, useState } from "react";
import React, {
FunctionComponent,
useCallback,
useEffect,
useState,
} from "react";
import { graphql } from "react-relay";
import { useLive, useVisibilityState } from "coral-framework/hooks";
import { withInView } from "coral-framework/lib/intersection";
import { useFetch, withFragmentContainer } from "coral-framework/lib/relay";
import { Icon } from "coral-ui/components/v2";
import { CallOut } from "coral-ui/components/v3";
@@ -15,76 +21,94 @@ import RefreshStoryViewerCount from "./RefreshStoryViewerCount";
import styles from "./ViewersWatchingContainer.css";
interface Props {
inView: boolean | undefined;
intersectionRef: React.Ref<any>;
story: ViewersWatchingContainer_story;
settings: ViewersWatchingContainer_settings;
}
const TIMEOUT = 20000;
const JITTER = 10000;
function getTimeout() {
return TIMEOUT + Math.floor(Math.random() * JITTER);
}
const TIMEOUT_JITTER = TIMEOUT / 2;
const MAX_TIMEOUT = TIMEOUT + TIMEOUT_JITTER;
const ViewersWatchingContainer: FunctionComponent<Props> = ({
story,
settings,
inView = false,
intersectionRef,
}) => {
const [lastRefreshed, setLastRefreshed] = useState<number>(Date.now());
const [refreshed, setRefreshed] = useState(false);
const live = useLive({ story, settings });
const visible = useVisibilityState();
const refreshStoryViewerCount = useFetch(RefreshStoryViewerCount);
// refresh will refresh the viewer count by refetching the data via the Graph.
const refresh = useCallback(async () => {
try {
// Refresh the viewer count!
await refreshStoryViewerCount({ storyID: story.id });
// Mark that we've refreshed (so we remove the extra +1).
setRefreshed(true);
// Mark the current date so it'll schedule the next timeout to run in the
// following useEffect.
setLastRefreshed(Date.now());
} catch (err) {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.error("couldn not refresh the story viewer count:", err);
}
}
}, [refreshStoryViewerCount, story.id]);
// available will be true when the viewer count is available.
const available = story.viewerCount !== null;
useEffect(() => {
// If we aren't live, we can't refresh the count cause there can't be any!
if (!live || !visible || !available) {
// If we aren't live, or there isn't a live count available (like if the
// feature flag isn't enabled), then we don't have to do anything! If the
// element isn't visible or the page isn't in the foreground, also halt
// updates.
if (!live || !available || !visible || !inView) {
return;
}
// Setup a timeout to be re-used.
let timeout: number | null = null;
let disposed = false;
// Get the time between now and the last time we updated. This addresses the
// issue where the timer was cleared and not reset because it was out of
// view so it'll fire right now.
const lastRefreshedDiff = Date.now() - lastRefreshed;
if (lastRefreshedDiff >= MAX_TIMEOUT) {
// The difference was greater than the max timeout. Fire the refresh right
// now.
void refresh();
return;
}
// Create a function that can be used to refresh the story viewer count.
const refresh = async () => {
try {
// Refresh the viewer count!
await refreshStoryViewerCount({ storyID: story.id });
const timeout = window.setTimeout(
refresh,
// Start with the max timeout...
MAX_TIMEOUT -
// Then subtract the difference from the last refresh date...
lastRefreshedDiff +
// And add a random jitter to help spread out the calls.
Math.floor(Math.random() * TIMEOUT_JITTER)
);
// If we're disposed, then stop now!
if (disposed) {
return;
}
// Mark that we've refreshed (so we remove the extra +1).
setRefreshed(true);
// Add this back with a timeout if we aren't disposed.
timeout = window.setTimeout(refresh, getTimeout());
} catch (err) {
if (process.env.NODE_ENV !== "production") {
// eslint-disable-next-line no-console
console.error("couldn not refresh the story viewer count:", err);
}
}
};
// Configure a timeout to fire later to refresh.
timeout = window.setTimeout(refresh, getTimeout());
// Clear the timeout when we dispose.
return () => {
// Clear the timeout if it's still running.
if (timeout) {
window.clearTimeout(timeout);
}
// Mark this as disposed so we don't update state after a refresh.
disposed = true;
window.clearTimeout(timeout);
};
}, [live, story.id, refreshStoryViewerCount, visible, available]);
}, [
live,
story.id,
refreshStoryViewerCount,
visible,
available,
inView,
lastRefreshed,
refresh,
]);
// If we aren't live or the viewer count isn't available, then return nothing!
if (!live || story.viewerCount === null) {
@@ -95,39 +119,43 @@ const ViewersWatchingContainer: FunctionComponent<Props> = ({
const viewerCount = refreshed ? story.viewerCount : story.viewerCount + 1;
return (
<CallOut
classes={{ icon: styles.icon, title: styles.title }}
icon={<Icon size="md">play_circle_filled</Icon>}
title={
<Localized id="comments-watchers" $count={viewerCount}>
<span>{viewerCount} people is online</span>
</Localized>
}
titleWeight="semiBold"
/>
<div ref={intersectionRef}>
<CallOut
classes={{ icon: styles.icon, title: styles.title }}
icon={<Icon size="md">play_circle_filled</Icon>}
title={
<Localized id="comments-watchers" $count={viewerCount}>
<span>{viewerCount} people is online</span>
</Localized>
}
titleWeight="semiBold"
/>
</div>
);
};
const enhanced = withFragmentContainer<Props>({
story: graphql`
fragment ViewersWatchingContainer_story on Story {
id
viewerCount
isClosed
settings {
live {
const enhanced = withInView(
withFragmentContainer<Props>({
story: graphql`
fragment ViewersWatchingContainer_story on Story {
id
viewerCount
isClosed
settings {
live {
enabled
}
}
}
`,
settings: graphql`
fragment ViewersWatchingContainer_settings on Settings {
disableCommenting {
enabled
}
}
}
`,
settings: graphql`
fragment ViewersWatchingContainer_settings on Settings {
disableCommenting {
enabled
}
}
`,
})(ViewersWatchingContainer);
`,
})(ViewersWatchingContainer)
);
export default enhanced;