Refactor StoryProgressSegment to have better controlled animations
This commit is contained in:
parent
a973c27fd4
commit
74b90a5cdd
16 changed files with 171 additions and 147 deletions
|
@ -172,6 +172,13 @@ const rules = {
|
||||||
'`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
|
'`with` is disallowed in strict mode because it makes code impossible to predict and optimize.',
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
|
|
||||||
|
'react-hooks/exhaustive-deps': [
|
||||||
|
'error',
|
||||||
|
{
|
||||||
|
additionalHooks: '^(useSpring|useSprings)$',
|
||||||
|
},
|
||||||
|
],
|
||||||
};
|
};
|
||||||
|
|
||||||
const typescriptRules = {
|
const typescriptRules = {
|
||||||
|
|
26
stylesheets/components/StoryProgressSegment.scss
Normal file
26
stylesheets/components/StoryProgressSegment.scss
Normal file
|
@ -0,0 +1,26 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
|
.StoryProgressSegment {
|
||||||
|
background: $color-white-alpha-40;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 2px;
|
||||||
|
margin-block: 12px 0;
|
||||||
|
margin-inline: 1px;
|
||||||
|
overflow: hidden;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.StoryProgressSegment__bar {
|
||||||
|
background: $color-white;
|
||||||
|
border-radius: 2px;
|
||||||
|
height: 100%;
|
||||||
|
&:dir(ltr) {
|
||||||
|
// stylelint-disable-next-line declaration-property-value-disallowed-list
|
||||||
|
transform: translateX(-100%);
|
||||||
|
}
|
||||||
|
&:dir(rtl) {
|
||||||
|
// stylelint-disable-next-line declaration-property-value-disallowed-list
|
||||||
|
transform: translateX(100%);
|
||||||
|
}
|
||||||
|
}
|
|
@ -303,30 +303,6 @@
|
||||||
|
|
||||||
&__progress {
|
&__progress {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|
||||||
&--container {
|
|
||||||
background: $color-white-alpha-40;
|
|
||||||
border-radius: 2px;
|
|
||||||
height: 2px;
|
|
||||||
margin-block: 12px 0;
|
|
||||||
margin-inline: 1px;
|
|
||||||
overflow: hidden;
|
|
||||||
width: 100%;
|
|
||||||
}
|
|
||||||
|
|
||||||
&--bar {
|
|
||||||
background: $color-white;
|
|
||||||
border-radius: 2px;
|
|
||||||
height: 100%;
|
|
||||||
&:dir(ltr) {
|
|
||||||
// stylelint-disable-next-line declaration-property-value-disallowed-list
|
|
||||||
transform: translateX(-100%);
|
|
||||||
}
|
|
||||||
&:dir(rtl) {
|
|
||||||
// stylelint-disable-next-line declaration-property-value-disallowed-list
|
|
||||||
transform: translateX(100%);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
&__animated-emojis {
|
&__animated-emojis {
|
||||||
|
|
|
@ -162,6 +162,7 @@
|
||||||
@import './components/StoryImage.scss';
|
@import './components/StoryImage.scss';
|
||||||
@import './components/StoryLinkPreview.scss';
|
@import './components/StoryLinkPreview.scss';
|
||||||
@import './components/StoryListItem.scss';
|
@import './components/StoryListItem.scss';
|
||||||
|
@import './components/StoryProgressSegment.scss';
|
||||||
@import './components/StoryReplyQuote.scss';
|
@import './components/StoryReplyQuote.scss';
|
||||||
@import './components/StoryViewer.scss';
|
@import './components/StoryViewer.scss';
|
||||||
@import './components/StoryViewsNRepliesModal.scss';
|
@import './components/StoryViewsNRepliesModal.scss';
|
||||||
|
|
|
@ -174,6 +174,7 @@ export function CallingRaisedHandsListButton({
|
||||||
}: CallingRaisedHandsListButtonPropsType): JSX.Element | null {
|
}: CallingRaisedHandsListButtonPropsType): JSX.Element | null {
|
||||||
const [isVisible, setIsVisible] = React.useState(raisedHandsCount > 0);
|
const [isVisible, setIsVisible] = React.useState(raisedHandsCount > 0);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [opacitySpringProps, opacitySpringApi] = useSpring(
|
const [opacitySpringProps, opacitySpringApi] = useSpring(
|
||||||
{
|
{
|
||||||
from: { opacity: 0 },
|
from: { opacity: 0 },
|
||||||
|
@ -182,6 +183,7 @@ export function CallingRaisedHandsListButton({
|
||||||
},
|
},
|
||||||
[]
|
[]
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [scaleSpringProps, scaleSpringApi] = useSpring(
|
const [scaleSpringProps, scaleSpringApi] = useSpring(
|
||||||
{
|
{
|
||||||
from: { scale: 0.9 },
|
from: { scale: 0.9 },
|
||||||
|
|
|
@ -322,6 +322,7 @@ export function Lightbox({
|
||||||
const thumbnailsMarginInlineStart =
|
const thumbnailsMarginInlineStart =
|
||||||
0 - (selectedIndex * THUMBNAIL_FULL_WIDTH + THUMBNAIL_WIDTH / 2);
|
0 - (selectedIndex * THUMBNAIL_FULL_WIDTH + THUMBNAIL_WIDTH / 2);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [thumbnailsStyle, thumbnailsAnimation] = useSpring(
|
const [thumbnailsStyle, thumbnailsAnimation] = useSpring(
|
||||||
{
|
{
|
||||||
config: THUMBNAIL_SPRING_CONFIG,
|
config: THUMBNAIL_SPRING_CONFIG,
|
||||||
|
|
|
@ -27,6 +27,7 @@ export type ButtonProps = {
|
||||||
export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
export const PlaybackButton = React.forwardRef<HTMLButtonElement, ButtonProps>(
|
||||||
function ButtonInner(props, ref) {
|
function ButtonInner(props, ref) {
|
||||||
const { mod, label, variant, onClick, context, visible = true } = props;
|
const { mod, label, variant, onClick, context, visible = true } = props;
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [animProps] = useSpring(
|
const [animProps] = useSpring(
|
||||||
{
|
{
|
||||||
config: SPRING_CONFIG,
|
config: SPRING_CONFIG,
|
||||||
|
|
|
@ -31,6 +31,7 @@ export function PlaybackRateButton({
|
||||||
}: Props): JSX.Element {
|
}: Props): JSX.Element {
|
||||||
const [isDown, setIsDown] = useState(false);
|
const [isDown, setIsDown] = useState(false);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [animProps] = useSpring(
|
const [animProps] = useSpring(
|
||||||
{
|
{
|
||||||
config: SPRING_CONFIG,
|
config: SPRING_CONFIG,
|
||||||
|
|
|
@ -124,7 +124,7 @@ export function StoryImage({
|
||||||
|
|
||||||
storyElement = (
|
storyElement = (
|
||||||
<video
|
<video
|
||||||
autoPlay
|
autoPlay={!isPaused}
|
||||||
className={getClassName('__image')}
|
className={getClassName('__image')}
|
||||||
controls={false}
|
controls={false}
|
||||||
key={attachment.url}
|
key={attachment.url}
|
||||||
|
|
65
ts/components/StoryProgressSegment.tsx
Normal file
65
ts/components/StoryProgressSegment.tsx
Normal file
|
@ -0,0 +1,65 @@
|
||||||
|
// Copyright 2024 Signal Messenger, LLC
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
import React, { memo, useEffect, useRef } from 'react';
|
||||||
|
import { animated, useSpring } from '@react-spring/web';
|
||||||
|
|
||||||
|
export type StoryProgressSegmentProps = Readonly<{
|
||||||
|
currentIndex: number;
|
||||||
|
duration: number | null;
|
||||||
|
index: number;
|
||||||
|
playing: boolean;
|
||||||
|
onFinish: () => void;
|
||||||
|
}>;
|
||||||
|
|
||||||
|
function isValidDuration(duration: number | null): boolean {
|
||||||
|
return duration != null && Number.isFinite(duration) && duration > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const StoryProgressSegment = memo(function StoryProgressSegment({
|
||||||
|
currentIndex,
|
||||||
|
duration,
|
||||||
|
index,
|
||||||
|
playing,
|
||||||
|
onFinish,
|
||||||
|
}: StoryProgressSegmentProps): JSX.Element {
|
||||||
|
const onFinishRef = useRef(onFinish);
|
||||||
|
useEffect(() => {
|
||||||
|
onFinishRef.current = onFinish;
|
||||||
|
}, [onFinish]);
|
||||||
|
|
||||||
|
const [progressBarStyle] = useSpring(() => {
|
||||||
|
return {
|
||||||
|
// Override default value from `Globals` to ignore "Reduce Motion" setting.
|
||||||
|
// This animation is important for progressing through stories and is minor
|
||||||
|
// enough that it shouldn't cause issues for users with sensitivity to motion.
|
||||||
|
skipAnimation: false,
|
||||||
|
immediate: index !== currentIndex,
|
||||||
|
// Pause while we are waiting for a valid duration
|
||||||
|
pause: !playing || !isValidDuration(duration),
|
||||||
|
from: { x: index < currentIndex ? 1 : 0 },
|
||||||
|
to: { x: index <= currentIndex ? 1 : 0 },
|
||||||
|
config: { duration: duration ?? Infinity },
|
||||||
|
onRest: result => {
|
||||||
|
if (index === currentIndex && result.finished) {
|
||||||
|
onFinishRef.current();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}, [index, playing, currentIndex, duration]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className="StoryProgressSegment"
|
||||||
|
aria-current={index === currentIndex ? 'step' : false}
|
||||||
|
>
|
||||||
|
<animated.div
|
||||||
|
className="StoryProgressSegment__bar"
|
||||||
|
style={{
|
||||||
|
transform: progressBarStyle.x.to(
|
||||||
|
value => `translateX(${value * 100 - 100}%)`
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
});
|
|
@ -3,13 +3,7 @@
|
||||||
|
|
||||||
import FocusTrap from 'focus-trap-react';
|
import FocusTrap from 'focus-trap-react';
|
||||||
import type { UIEvent } from 'react';
|
import type { UIEvent } from 'react';
|
||||||
import React, {
|
import React, { useCallback, useEffect, useMemo, useState } from 'react';
|
||||||
useCallback,
|
|
||||||
useEffect,
|
|
||||||
useMemo,
|
|
||||||
useRef,
|
|
||||||
useState,
|
|
||||||
} from 'react';
|
|
||||||
import classNames from 'classnames';
|
import classNames from 'classnames';
|
||||||
import type { DraftBodyRanges } from '../types/BodyRange';
|
import type { DraftBodyRanges } from '../types/BodyRange';
|
||||||
import type { LocalizerType } from '../types/Util';
|
import type { LocalizerType } from '../types/Util';
|
||||||
|
@ -59,6 +53,7 @@ import { MessageBody } from './conversation/MessageBody';
|
||||||
import { RenderLocation } from './conversation/MessageTextRenderer';
|
import { RenderLocation } from './conversation/MessageTextRenderer';
|
||||||
import { arrow } from '../util/keyboard';
|
import { arrow } from '../util/keyboard';
|
||||||
import { useElementId } from '../hooks/useUniqueId';
|
import { useElementId } from '../hooks/useUniqueId';
|
||||||
|
import { StoryProgressSegment } from './StoryProgressSegment';
|
||||||
|
|
||||||
function renderStrong(parts: Array<JSX.Element | string>) {
|
function renderStrong(parts: Array<JSX.Element | string>) {
|
||||||
return <strong>{parts}</strong>;
|
return <strong>{parts}</strong>;
|
||||||
|
@ -294,70 +289,9 @@ export function StoryViewer({
|
||||||
};
|
};
|
||||||
}, [attachment, messageId]);
|
}, [attachment, messageId]);
|
||||||
|
|
||||||
const progressBarRef = useRef<HTMLDivElement>(null);
|
|
||||||
const animationRef = useRef<Animation | null>(null);
|
|
||||||
|
|
||||||
// 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(() => {
|
|
||||||
onFinishRef.current = () => {
|
|
||||||
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
|
// 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");
|
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) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
strictAssert(
|
|
||||||
progressBarRef.current != null,
|
|
||||||
"progressBarRef can't be null"
|
|
||||||
);
|
|
||||||
const target = progressBarRef.current;
|
|
||||||
|
|
||||||
const animation = target.animate(
|
|
||||||
[{ transform: 'translateX(-100%)' }, { transform: 'translateX(0%)' }],
|
|
||||||
{
|
|
||||||
id: 'story-progress-bar',
|
|
||||||
duration: storyDuration,
|
|
||||||
easing: 'linear',
|
|
||||||
fill: 'forwards',
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
animationRef.current = animation;
|
|
||||||
|
|
||||||
function onFinish() {
|
|
||||||
onFinishRef.current?.();
|
|
||||||
}
|
|
||||||
|
|
||||||
animation.addEventListener('finish', onFinish);
|
|
||||||
|
|
||||||
// Reset the stuff that pauses a story when you switch story views
|
|
||||||
setConfirmDeleteStory(undefined);
|
|
||||||
setHasConfirmHideStory(false);
|
|
||||||
setHasExpandedCaption(false);
|
|
||||||
setIsSpoilerExpanded({});
|
|
||||||
setIsShowingContextMenu(false);
|
|
||||||
setPauseStory(false);
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
animation.removeEventListener('finish', onFinish);
|
|
||||||
animation.cancel();
|
|
||||||
};
|
|
||||||
}, [story.messageId, storyDuration]);
|
|
||||||
|
|
||||||
const [pauseStory, setPauseStory] = useState(false);
|
const [pauseStory, setPauseStory] = useState(false);
|
||||||
const [pressing, setPressing] = useState(false);
|
const [pressing, setPressing] = useState(false);
|
||||||
const [longPress, setLongPress] = useState(false);
|
const [longPress, setLongPress] = useState(false);
|
||||||
|
@ -384,9 +318,21 @@ export function StoryViewer({
|
||||||
}
|
}
|
||||||
}, [isWindowActive]);
|
}, [isWindowActive]);
|
||||||
|
|
||||||
|
// Reset the stuff that pauses a story when you switch story views
|
||||||
|
useEffect(() => {
|
||||||
|
setConfirmDeleteStory(undefined);
|
||||||
|
setHasConfirmHideStory(false);
|
||||||
|
setHasExpandedCaption(false);
|
||||||
|
setIsSpoilerExpanded({});
|
||||||
|
setIsShowingContextMenu(false);
|
||||||
|
setPauseStory(false);
|
||||||
|
setStoryDuration(undefined);
|
||||||
|
}, [story.messageId]);
|
||||||
|
|
||||||
const alertElement = renderAlert();
|
const alertElement = renderAlert();
|
||||||
|
|
||||||
const shouldPauseViewing =
|
const shouldPauseViewing =
|
||||||
|
storyDuration == null ||
|
||||||
Boolean(alertElement) ||
|
Boolean(alertElement) ||
|
||||||
Boolean(confirmDeleteStory) ||
|
Boolean(confirmDeleteStory) ||
|
||||||
currentViewTarget != null ||
|
currentViewTarget != null ||
|
||||||
|
@ -398,14 +344,6 @@ export function StoryViewer({
|
||||||
Boolean(reactionEmoji) ||
|
Boolean(reactionEmoji) ||
|
||||||
pressing;
|
pressing;
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (shouldPauseViewing) {
|
|
||||||
animationRef.current?.pause();
|
|
||||||
} else {
|
|
||||||
animationRef.current?.play();
|
|
||||||
}
|
|
||||||
}, [shouldPauseViewing, story.messageId, storyDuration]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
markStoryRead(messageId);
|
markStoryRead(messageId);
|
||||||
log.info('stories.markStoryRead', { message: messageIdForLogging });
|
log.info('stories.markStoryRead', { message: messageIdForLogging });
|
||||||
|
@ -890,25 +828,20 @@ export function StoryViewer({
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__progress" {...stopPauseBubblingProps}>
|
<div className="StoryViewer__progress" {...stopPauseBubblingProps}>
|
||||||
{Array.from(Array(numStories), (_, index) => (
|
{Array.from(Array(numStories), (_, index) => (
|
||||||
<div className="StoryViewer__progress--container" key={index}>
|
<StoryProgressSegment
|
||||||
{currentIndex === index ? (
|
key={`${story.messageId}-${index}-${currentIndex}`}
|
||||||
<div
|
index={index}
|
||||||
ref={progressBarRef}
|
currentIndex={currentIndex}
|
||||||
className="StoryViewer__progress--bar"
|
duration={storyDuration ?? null}
|
||||||
/>
|
playing={!shouldPauseViewing}
|
||||||
) : (
|
onFinish={() => {
|
||||||
<div
|
viewStory({
|
||||||
className="StoryViewer__progress--bar"
|
storyId: story.messageId,
|
||||||
style={
|
storyViewMode,
|
||||||
currentIndex < index
|
viewDirection: StoryViewDirectionType.Next,
|
||||||
? {}
|
});
|
||||||
: {
|
}}
|
||||||
transform: 'translateX(0%)',
|
/>
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
<div className="StoryViewer__actions" {...stopPauseBubblingProps}>
|
<div className="StoryViewer__actions" {...stopPauseBubblingProps}>
|
||||||
|
|
|
@ -96,6 +96,7 @@ function PlayedDot({
|
||||||
const start = played ? 1 : 0;
|
const start = played ? 1 : 0;
|
||||||
const end = played ? 0 : 1;
|
const end = played ? 0 : 1;
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [animProps] = useSpring(
|
const [animProps] = useSpring(
|
||||||
{
|
{
|
||||||
config: SPRING_CONFIG,
|
config: SPRING_CONFIG,
|
||||||
|
|
|
@ -47,6 +47,7 @@ export function TimelineFloatingHeader({
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}),
|
}),
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
[isLoading]
|
[isLoading]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
|
@ -87,6 +87,7 @@ function TypingBubbleAvatar({
|
||||||
i18n: LocalizerType;
|
i18n: LocalizerType;
|
||||||
theme: ThemeType;
|
theme: ThemeType;
|
||||||
}): ReactElement | null {
|
}): ReactElement | null {
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [springProps, springApi] = useSpring(
|
const [springProps, springApi] = useSpring(
|
||||||
{
|
{
|
||||||
config: SPRING_CONFIG,
|
config: SPRING_CONFIG,
|
||||||
|
@ -287,6 +288,7 @@ export function TypingBubble({
|
||||||
[typingContactIds]
|
[typingContactIds]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [outerDivStyle, outerDivSpringApi] = useSpring(
|
const [outerDivStyle, outerDivSpringApi] = useSpring(
|
||||||
{
|
{
|
||||||
to: OUTER_DIV_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
to: OUTER_DIV_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
||||||
|
@ -294,6 +296,7 @@ export function TypingBubble({
|
||||||
},
|
},
|
||||||
[isSomeoneTyping]
|
[isSomeoneTyping]
|
||||||
);
|
);
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps -- FIXME
|
||||||
const [typingAnimationStyle, typingAnimationSpringApi] = useSpring(
|
const [typingAnimationStyle, typingAnimationSpringApi] = useSpring(
|
||||||
{
|
{
|
||||||
to: BUBBLE_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
to: BUBBLE_ANIMATION_PROPS[isSomeoneTyping ? 'visible' : 'hidden'],
|
||||||
|
|
|
@ -11,6 +11,8 @@ import {
|
||||||
} from '../types/Attachment';
|
} from '../types/Attachment';
|
||||||
import { count } from './grapheme';
|
import { count } from './grapheme';
|
||||||
import { SECOND } from './durations';
|
import { SECOND } from './durations';
|
||||||
|
import * as log from '../logging/log';
|
||||||
|
import * as Errors from '../types/errors';
|
||||||
|
|
||||||
const DEFAULT_DURATION = 5 * SECOND;
|
const DEFAULT_DURATION = 5 * SECOND;
|
||||||
const MAX_VIDEO_DURATION = 30 * SECOND;
|
const MAX_VIDEO_DURATION = 30 * SECOND;
|
||||||
|
@ -42,21 +44,39 @@ export async function getStoryDuration(
|
||||||
|
|
||||||
if (isGIF([attachment]) || isVideo([attachment])) {
|
if (isGIF([attachment]) || isVideo([attachment])) {
|
||||||
const videoEl = document.createElement('video');
|
const videoEl = document.createElement('video');
|
||||||
if (!attachment.url) {
|
const { url } = attachment;
|
||||||
|
|
||||||
|
if (!url) {
|
||||||
return DEFAULT_DURATION;
|
return DEFAULT_DURATION;
|
||||||
}
|
}
|
||||||
videoEl.src = attachment.url;
|
|
||||||
|
|
||||||
await new Promise<void>(resolve => {
|
let duration: number;
|
||||||
function resolveAndRemove() {
|
try {
|
||||||
resolve();
|
duration = await new Promise<number>((resolve, reject) => {
|
||||||
videoEl.removeEventListener('loadedmetadata', resolveAndRemove);
|
function resolveAndRemove() {
|
||||||
}
|
resolve(videoEl.duration * SECOND);
|
||||||
|
videoEl.removeEventListener('loadedmetadata', resolveAndRemove);
|
||||||
|
}
|
||||||
|
|
||||||
videoEl.addEventListener('loadedmetadata', resolveAndRemove);
|
videoEl.addEventListener('loadedmetadata', resolveAndRemove);
|
||||||
});
|
videoEl.addEventListener('error', () => {
|
||||||
|
reject(videoEl.error ?? new Error('Failed to load'));
|
||||||
|
});
|
||||||
|
|
||||||
const duration = Math.ceil(videoEl.duration * SECOND);
|
videoEl.src = url;
|
||||||
|
});
|
||||||
|
} catch (error) {
|
||||||
|
log.error(
|
||||||
|
'getStoryDuration: Failed to load video duration',
|
||||||
|
Errors.toLogFormat(error)
|
||||||
|
);
|
||||||
|
return DEFAULT_DURATION;
|
||||||
|
} finally {
|
||||||
|
// Stop loading video
|
||||||
|
videoEl.pause();
|
||||||
|
videoEl.removeAttribute('src'); // empty source
|
||||||
|
videoEl.load();
|
||||||
|
}
|
||||||
|
|
||||||
if (isGIF([attachment])) {
|
if (isGIF([attachment])) {
|
||||||
// GIFs: Loop gifs 3 times or play for 5 seconds, whichever is longer.
|
// GIFs: Loop gifs 3 times or play for 5 seconds, whichever is longer.
|
||||||
|
|
|
@ -2882,24 +2882,10 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
"path": "ts/components/StoryViewer.tsx",
|
"path": "ts/components/StoryProgressSegment.tsx",
|
||||||
"line": " const progressBarRef = useRef<HTMLDivElement>(null);",
|
"line": " const onFinishRef = useRef(onFinish);",
|
||||||
"reasonCategory": "usageTrusted",
|
"reasonCategory": "usageTrusted",
|
||||||
"updated": "2022-10-13T15:18:21.267Z"
|
"updated": "2024-08-13T20:48:09.226Z"
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/StoryViewer.tsx",
|
|
||||||
"line": " const animationRef = useRef<Animation | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-10-13T15:18:21.267Z"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"rule": "React-useRef",
|
|
||||||
"path": "ts/components/StoryViewer.tsx",
|
|
||||||
"line": " const onFinishRef = useRef<(() => void) | null>(null);",
|
|
||||||
"reasonCategory": "usageTrusted",
|
|
||||||
"updated": "2022-10-13T15:18:21.267Z"
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"rule": "React-useRef",
|
"rule": "React-useRef",
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue