stories: use web animations api to simplify progress bar/playback

This commit is contained in:
Jamie Kyle 2022-10-13 14:40:43 -07:00 committed by GitHub
parent b2792639aa
commit 9ee0502553
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
3 changed files with 76 additions and 81 deletions

View file

@ -67,6 +67,9 @@ export default {
toggleHasAllStoriesMuted: { action: true }, toggleHasAllStoriesMuted: { action: true },
viewStory: { action: true }, viewStory: { action: true },
}, },
args: {
currentIndex: 0,
},
} as Meta; } as Meta;
const Template: Story<PropsType> = args => <StoryViewer {...args} />; const Template: Story<PropsType> = args => <StoryViewer {...args} />;

View file

@ -10,7 +10,6 @@ import React, {
useState, useState,
} from 'react'; } from 'react';
import classNames from 'classnames'; import classNames from 'classnames';
import { Globals, useSpring, animated, to } from '@react-spring/web';
import type { BodyRangeType, LocalizerType } from '../types/Util'; import type { BodyRangeType, LocalizerType } from '../types/Util';
import type { ContextMenuOptionType } from './ContextMenu'; import type { ContextMenuOptionType } from './ContextMenu';
import type { ConversationType } from '../state/ducks/conversations'; import type { ConversationType } from '../state/ducks/conversations';
@ -45,6 +44,7 @@ import { getStoryDuration } from '../util/getStoryDuration';
import { graphemeAwareSlice } from '../util/graphemeAwareSlice'; import { graphemeAwareSlice } from '../util/graphemeAwareSlice';
import { isVideoAttachment } from '../types/Attachment'; import { isVideoAttachment } from '../types/Attachment';
import { useEscapeHandling } from '../hooks/useEscapeHandling'; import { useEscapeHandling } from '../hooks/useEscapeHandling';
import { strictAssert } from '../util/assert';
export type PropsType = { export type PropsType = {
currentIndex: number; currentIndex: number;
@ -242,77 +242,54 @@ export const StoryViewer = ({
}; };
}, [attachment, messageId]); }, [attachment, messageId]);
const unmountRef = useRef<boolean>(false); const progressBarRef = useRef<HTMLDivElement>(null);
useEffect(() => { const animationRef = useRef<Animation | null>(null);
return () => {
unmountRef.current = true;
};
}, []);
// Currently there's no way to globally skip animations but only allow select // Putting this in a ref allows us to call it from the useEffect below without
// ones. This component temporarily overrides the skipAnimation global and // triggering the effect to re-run every time these values change.
// then sets it back when it unmounts. const onFinishRef = useRef<(() => void) | null>(null);
// https://github.com/pmndrs/react-spring/issues/1982
useEffect(() => { useEffect(() => {
const { skipAnimation } = Globals; onFinishRef.current = () => {
Globals.assign({ viewStory({
skipAnimation: false, storyId: story.messageId,
}); storyViewMode,
viewDirection: StoryViewDirectionType.Next,
return () => {
Globals.assign({
skipAnimation,
}); });
}; };
}, []); }, [story.messageId, storyViewMode, viewStory]);
const [styles, spring] = useSpring( // This guarantees that we'll have a valid ref to the animation when we need it
() => ({ strictAssert(currentIndex != null, "StoryViewer: currentIndex can't be null");
from: { width: 0 },
to: { width: 100 },
loop: true,
onRest: {
width: ({ value }) => {
if (unmountRef.current) {
log.info(
'stories.StoryViewer.spring.onRest: called after component unmounted'
);
return;
}
if (value === 100) {
viewStory({
storyId: story.messageId,
storyViewMode,
viewDirection: StoryViewDirectionType.Next,
});
}
},
},
}),
[story.messageId, storyViewMode, viewStory]
);
// We need to be careful about this effect refreshing, it should only run // We need to be careful about this effect refreshing, it should only run
// every time a story changes or its duration changes. // every time a story changes or its duration changes.
useEffect(() => { useEffect(() => {
if (!storyDuration) { strictAssert(
spring.stop(); progressBarRef.current != null,
return; "progressBarRef can't be null"
} );
const target = progressBarRef.current;
spring.start({ const animation = target.animate([{ width: '0%' }, { width: '100%' }], {
config: { id: 'story-progress-bar',
duration: storyDuration, duration: storyDuration,
}, easing: 'linear',
from: { width: 0 }, fill: 'forwards',
to: { width: 100 },
}); });
animationRef.current = animation;
function onFinish() {
onFinishRef.current?.();
}
animation.addEventListener('finish', onFinish);
return () => { return () => {
spring.stop(); animation.removeEventListener('finish', onFinish);
animation.cancel();
}; };
}, [currentIndex, spring, storyDuration]); }, [story.messageId, storyDuration]);
const [pauseStory, setPauseStory] = useState(false); const [pauseStory, setPauseStory] = useState(false);
@ -327,11 +304,11 @@ export const StoryViewer = ({
useEffect(() => { useEffect(() => {
if (shouldPauseViewing) { if (shouldPauseViewing) {
spring.pause(); animationRef.current?.pause();
} else { } else {
spring.resume(); animationRef.current?.play();
} }
}, [shouldPauseViewing, spring]); }, [shouldPauseViewing]);
useEffect(() => { useEffect(() => {
markStoryRead(messageId); markStoryRead(messageId);
@ -710,11 +687,9 @@ export const StoryViewer = ({
{Array.from(Array(numStories), (_, index) => ( {Array.from(Array(numStories), (_, index) => (
<div className="StoryViewer__progress--container" key={index}> <div className="StoryViewer__progress--container" key={index}>
{currentIndex === index ? ( {currentIndex === index ? (
<animated.div <div
ref={progressBarRef}
className="StoryViewer__progress--bar" className="StoryViewer__progress--bar"
style={{
width: to([styles.width], width => `${width}%`),
}}
/> />
) : ( ) : (
<div <div

View file

@ -9298,9 +9298,26 @@
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/StoryViewer.tsx", "path": "ts/components/StoryViewer.tsx",
"line": " const unmountRef = useRef<boolean>(false);", "line": " const progressBarRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2022-07-18T23:30:04.033Z" "updated": "2022-10-13T15:18:21.267Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/components/StoryViewer.tsx",
"line": " const animationRef = useRef<Animation | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-10-13T15:18:21.267Z",
"reasonDetail": "<optional>"
},
{
"rule": "React-useRef",
"path": "ts/components/StoryViewer.tsx",
"line": " const onFinishRef = useRef<(() => void) | null>(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-10-13T15:18:21.267Z",
"reasonDetail": "<optional>"
}, },
{ {
"rule": "React-useRef", "rule": "React-useRef",
@ -9316,6 +9333,20 @@
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2022-08-04T00:52:01.080Z" "updated": "2022-08-04T00:52:01.080Z"
}, },
{
"rule": "React-useRef",
"path": "ts/components/StoryViewsNRepliesModal.tsx",
"line": " const shouldScrollToBottomRef = useRef(true);",
"reasonCategory": "usageTrusted",
"updated": "2022-10-05T18:51:56.411Z"
},
{
"rule": "React-useRef",
"path": "ts/components/StoryViewsNRepliesModal.tsx",
"line": " const bottomRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "usageTrusted",
"updated": "2022-10-05T18:51:56.411Z"
},
{ {
"rule": "React-useRef", "rule": "React-useRef",
"path": "ts/components/TextAttachment.tsx", "path": "ts/components/TextAttachment.tsx",
@ -9683,19 +9714,5 @@
"line": " message.innerHTML = window.SignalContext.i18n('optimizingApplication');", "line": " message.innerHTML = window.SignalContext.i18n('optimizingApplication');",
"reasonCategory": "usageTrusted", "reasonCategory": "usageTrusted",
"updated": "2021-09-17T21:02:59.414Z" "updated": "2021-09-17T21:02:59.414Z"
},
{
"rule": "React-useRef",
"path": "ts/components/StoryViewsNRepliesModal.tsx",
"line": " const shouldScrollToBottomRef = useRef(true);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2022-10-05T18:51:56.411Z"
},
{
"rule": "React-useRef",
"path": "ts/components/StoryViewsNRepliesModal.tsx",
"line": " const bottomRef = useRef<HTMLDivElement>(null);",
"reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted",
"updated": "2022-10-05T18:51:56.411Z"
} }
] ]