stories: use web animations api to simplify progress bar/playback
This commit is contained in:
parent
b2792639aa
commit
9ee0502553
3 changed files with 76 additions and 81 deletions
|
@ -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} />;
|
||||||
|
|
|
@ -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
|
||||||
|
|
|
@ -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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue