From 9ee0502553771f4d9233e953d45baadadd75f081 Mon Sep 17 00:00:00 2001 From: Jamie Kyle <113370520+jamiebuilds-signal@users.noreply.github.com> Date: Thu, 13 Oct 2022 14:40:43 -0700 Subject: [PATCH] stories: use web animations api to simplify progress bar/playback --- ts/components/StoryViewer.stories.tsx | 3 + ts/components/StoryViewer.tsx | 105 ++++++++++---------------- ts/util/lint/exceptions.json | 49 ++++++++---- 3 files changed, 76 insertions(+), 81 deletions(-) diff --git a/ts/components/StoryViewer.stories.tsx b/ts/components/StoryViewer.stories.tsx index 6647d4451..5df182958 100644 --- a/ts/components/StoryViewer.stories.tsx +++ b/ts/components/StoryViewer.stories.tsx @@ -67,6 +67,9 @@ export default { toggleHasAllStoriesMuted: { action: true }, viewStory: { action: true }, }, + args: { + currentIndex: 0, + }, } as Meta; const Template: Story = args => ; diff --git a/ts/components/StoryViewer.tsx b/ts/components/StoryViewer.tsx index 503f59a54..27b5fe379 100644 --- a/ts/components/StoryViewer.tsx +++ b/ts/components/StoryViewer.tsx @@ -10,7 +10,6 @@ import React, { useState, } from 'react'; import classNames from 'classnames'; -import { Globals, useSpring, animated, to } from '@react-spring/web'; import type { BodyRangeType, LocalizerType } from '../types/Util'; import type { ContextMenuOptionType } from './ContextMenu'; import type { ConversationType } from '../state/ducks/conversations'; @@ -45,6 +44,7 @@ import { getStoryDuration } from '../util/getStoryDuration'; import { graphemeAwareSlice } from '../util/graphemeAwareSlice'; import { isVideoAttachment } from '../types/Attachment'; import { useEscapeHandling } from '../hooks/useEscapeHandling'; +import { strictAssert } from '../util/assert'; export type PropsType = { currentIndex: number; @@ -242,77 +242,54 @@ export const StoryViewer = ({ }; }, [attachment, messageId]); - const unmountRef = useRef(false); - useEffect(() => { - return () => { - unmountRef.current = true; - }; - }, []); + const progressBarRef = useRef(null); + const animationRef = useRef(null); - // Currently there's no way to globally skip animations but only allow select - // ones. This component temporarily overrides the skipAnimation global and - // then sets it back when it unmounts. - // https://github.com/pmndrs/react-spring/issues/1982 + // Putting this in a ref allows us to call it from the useEffect below without + // triggering the effect to re-run every time these values change. + const onFinishRef = useRef<(() => void) | null>(null); useEffect(() => { - const { skipAnimation } = Globals; - Globals.assign({ - skipAnimation: false, - }); - - return () => { - Globals.assign({ - skipAnimation, + onFinishRef.current = () => { + viewStory({ + storyId: story.messageId, + storyViewMode, + viewDirection: StoryViewDirectionType.Next, }); }; - }, []); + }, [story.messageId, storyViewMode, viewStory]); - const [styles, spring] = useSpring( - () => ({ - 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] - ); + // 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"); // We need to be careful about this effect refreshing, it should only run // every time a story changes or its duration changes. useEffect(() => { - if (!storyDuration) { - spring.stop(); - return; - } + strictAssert( + progressBarRef.current != null, + "progressBarRef can't be null" + ); + const target = progressBarRef.current; - spring.start({ - config: { - duration: storyDuration, - }, - from: { width: 0 }, - to: { width: 100 }, + const animation = target.animate([{ width: '0%' }, { width: '100%' }], { + id: 'story-progress-bar', + duration: storyDuration, + easing: 'linear', + fill: 'forwards', }); + animationRef.current = animation; + + function onFinish() { + onFinishRef.current?.(); + } + + animation.addEventListener('finish', onFinish); + return () => { - spring.stop(); + animation.removeEventListener('finish', onFinish); + animation.cancel(); }; - }, [currentIndex, spring, storyDuration]); + }, [story.messageId, storyDuration]); const [pauseStory, setPauseStory] = useState(false); @@ -327,11 +304,11 @@ export const StoryViewer = ({ useEffect(() => { if (shouldPauseViewing) { - spring.pause(); + animationRef.current?.pause(); } else { - spring.resume(); + animationRef.current?.play(); } - }, [shouldPauseViewing, spring]); + }, [shouldPauseViewing]); useEffect(() => { markStoryRead(messageId); @@ -710,11 +687,9 @@ export const StoryViewer = ({ {Array.from(Array(numStories), (_, index) => (
{currentIndex === index ? ( - `${width}%`), - }} /> ) : (
(false);", + "line": " const progressBarRef = useRef(null);", "reasonCategory": "usageTrusted", - "updated": "2022-07-18T23:30:04.033Z" + "updated": "2022-10-13T15:18:21.267Z", + "reasonDetail": "" + }, + { + "rule": "React-useRef", + "path": "ts/components/StoryViewer.tsx", + "line": " const animationRef = useRef(null);", + "reasonCategory": "usageTrusted", + "updated": "2022-10-13T15:18:21.267Z", + "reasonDetail": "" + }, + { + "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": "" }, { "rule": "React-useRef", @@ -9316,6 +9333,20 @@ "reasonCategory": "usageTrusted", "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(null);", + "reasonCategory": "usageTrusted", + "updated": "2022-10-05T18:51:56.411Z" + }, { "rule": "React-useRef", "path": "ts/components/TextAttachment.tsx", @@ -9683,19 +9714,5 @@ "line": " message.innerHTML = window.SignalContext.i18n('optimizingApplication');", "reasonCategory": "usageTrusted", "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(null);", - "reasonCategory": "falseMatch|testCode|exampleCode|otherUtilityCode|regexMatchedSafeCode|notExercisedByOurApp|ruleNeeded|usageTrusted", - "updated": "2022-10-05T18:51:56.411Z" } ]