mirror of
https://github.com/fluxerapp/fluxer
synced 2026-10-17 13:05:32 +09:00
fix(app): backport rendering improvements (#1580)
This commit is contained in:
1 parent
0c291a01da
commit
42cdc1f877
513 files changed
+98743
-70397
No files matched your search
@@ -4,7 +4,6 @@ import Accessibility from '@app/features/accessibility/state/Accessibility';
|
||||
import {useAnimatedImageDecoder} from '@app/features/app/hooks/useAnimatedImageDecoder';
|
||||
import {
|
||||
getAnimatedMediaPlaybackAllowed,
|
||||
subscribeAnimatedMediaPlaybackChange,
|
||||
useAnimatedMediaPlaybackAllowed,
|
||||
} from '@app/features/app/hooks/useAnimatedMediaPlayback';
|
||||
import {useShouldAnimate} from '@app/features/app/hooks/useShouldAnimate';
|
||||
@@ -13,6 +12,14 @@ import embedStyles from '@app/features/channel/components/embeds/ChannelEmbed.mo
|
||||
import {deriveDefaultNameFromMessage} from '@app/features/channel/components/embeds/EmbedUtils';
|
||||
import {MatureMediaBlurOverlay} from '@app/features/channel/components/embeds/MatureMediaBlurOverlay';
|
||||
import styles from '@app/features/channel/components/embeds/media/EmbedGifv.module.css';
|
||||
import {
|
||||
createInitialGIFVRenderState,
|
||||
GIFVPlaybackState,
|
||||
GIFVRenderEventType,
|
||||
GIFVVideoState,
|
||||
getGIFVRenderLayers,
|
||||
reduceGIFVRenderState,
|
||||
} from '@app/features/channel/components/embeds/media/GIFVRenderStateMachine';
|
||||
import {GifIndicator} from '@app/features/channel/components/embeds/media/GifIndicator';
|
||||
import {getMediaButtonVisibility} from '@app/features/channel/components/embeds/media/MediaButtonUtils';
|
||||
import {MediaContainer, shouldShowOverlays} from '@app/features/channel/components/embeds/media/MediaContainer';
|
||||
@@ -29,11 +36,11 @@ import {useNearViewport} from '@app/features/messaging/hooks/useNearViewport';
|
||||
import {useOpenInBrowserOnMiddleClick} from '@app/features/messaging/hooks/useOpenInBrowserOnMiddleClick';
|
||||
import type {Message} from '@app/features/messaging/models/MessagingMessage';
|
||||
import {createDownloadHandler} from '@app/features/messaging/utils/FileDownloadUtils';
|
||||
import * as ImageCacheUtils from '@app/features/messaging/utils/ImageCacheUtils';
|
||||
import {getEmbedMediaDimensions} from '@app/features/messaging/utils/MediaDimensionConfig';
|
||||
import {
|
||||
buildFittedAnimatedImageProxyURL,
|
||||
buildFittedStaticGifPreviewURL,
|
||||
buildMediaProxyURL,
|
||||
stripMediaProxyParams,
|
||||
} from '@app/features/messaging/utils/MediaProxyUtils';
|
||||
import {remFromPx} from '@app/features/theme/layout/RemFromPx';
|
||||
@@ -43,13 +50,12 @@ import * as MediaViewerCommands from '@app/features/ui/commands/MediaViewerComma
|
||||
import MediaViewer from '@app/features/ui/state/MediaViewer';
|
||||
import MobileLayout from '@app/features/ui/state/MobileLayout';
|
||||
import {createCalculator} from '@app/features/ui/utils/DimensionUtils';
|
||||
import KlipyWatermarkSvg from '@app/media/images/klipy-watermark.svg?react';
|
||||
import {msg} from '@lingui/core/macro';
|
||||
import {useLingui} from '@lingui/react/macro';
|
||||
import {clsx} from 'clsx';
|
||||
import {motion} from 'framer-motion';
|
||||
import {observer} from 'mobx-react-lite';
|
||||
import {type FC, useCallback, useEffect, useMemo, useRef, useState} from 'react';
|
||||
import {type FC, useCallback, useEffect, useLayoutEffect, useMemo, useReducer, useRef, useState} from 'react';
|
||||
|
||||
const OPEN_ANIMATED_GIF_VIDEO_IN_FULL_VIEW_DESCRIPTOR = msg({
|
||||
message: 'Open animated GIF video in full view',
|
||||
@@ -78,11 +84,68 @@ const ANIMATED_GIF_DESCRIPTOR = msg({
|
||||
const EMBED_MEDIA_FADE_DURATION_SECONDS = 0.08;
|
||||
const DECODER_MAX_CACHED_FRAMES = 24;
|
||||
|
||||
type AnimatedImagePreloadStatus = 'idle' | 'loading' | 'ready' | 'failed';
|
||||
|
||||
interface AnimatedImagePreloadState {
|
||||
url: string;
|
||||
status: AnimatedImagePreloadStatus;
|
||||
}
|
||||
|
||||
function watchPresentedVideoFrame(video: HTMLVideoElement, onPresented: () => void): () => void {
|
||||
let cancelled = false;
|
||||
const requestVideoFrameCallback = video.requestVideoFrameCallback;
|
||||
if (typeof requestVideoFrameCallback === 'function') {
|
||||
const callbackHandle = requestVideoFrameCallback.call(video, () => {
|
||||
if (!cancelled) onPresented();
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
const cancelVideoFrameCallback = video.cancelVideoFrameCallback;
|
||||
if (typeof cancelVideoFrameCallback === 'function') {
|
||||
cancelVideoFrameCallback.call(video, callbackHandle);
|
||||
}
|
||||
};
|
||||
}
|
||||
const ownerWindow = video.ownerDocument.defaultView;
|
||||
if (ownerWindow == null) return () => {};
|
||||
let secondFrameHandle: number | null = null;
|
||||
const firstFrameHandle = ownerWindow.requestAnimationFrame(() => {
|
||||
secondFrameHandle = ownerWindow.requestAnimationFrame(() => {
|
||||
if (!cancelled) onPresented();
|
||||
});
|
||||
});
|
||||
return () => {
|
||||
cancelled = true;
|
||||
ownerWindow.cancelAnimationFrame(firstFrameHandle);
|
||||
if (secondFrameHandle != null) ownerWindow.cancelAnimationFrame(secondFrameHandle);
|
||||
};
|
||||
}
|
||||
|
||||
function mediaElementHasSource(element: HTMLImageElement | HTMLVideoElement, source: string): boolean {
|
||||
if (element.currentSrc.length > 0) {
|
||||
try {
|
||||
return element.currentSrc === new URL(source, element.ownerDocument.baseURI).href;
|
||||
} catch {
|
||||
return element.currentSrc === source;
|
||||
}
|
||||
}
|
||||
const attributeSource = element.getAttribute('src');
|
||||
if (attributeSource === source) return true;
|
||||
return element.src === source;
|
||||
}
|
||||
|
||||
function completeImageHasSource(image: HTMLImageElement | null, source: string): image is HTMLImageElement {
|
||||
if (image == null) return false;
|
||||
if (!mediaElementHasSource(image, source)) return false;
|
||||
return image.complete && image.naturalWidth > 0 && image.naturalHeight > 0;
|
||||
}
|
||||
|
||||
type GifvEmbedProps = BaseMediaProps & {
|
||||
embedURL: string;
|
||||
naturalWidth: number;
|
||||
naturalHeight: number;
|
||||
placeholder?: string;
|
||||
thumbnailProxyURL?: string;
|
||||
alt?: string | null;
|
||||
};
|
||||
|
||||
@@ -316,6 +379,7 @@ export const EmbedGifv: FC<
|
||||
({
|
||||
embedURL,
|
||||
videoProxyURL,
|
||||
thumbnailProxyURL,
|
||||
alt,
|
||||
naturalWidth,
|
||||
naturalHeight,
|
||||
@@ -337,19 +401,38 @@ export const EmbedGifv: FC<
|
||||
const messageViewContext = useMaybeMessageViewContext();
|
||||
const mediaCalculator = useEmbedMediaCalculator();
|
||||
const videoRef = useRef<HTMLVideoElement>(null);
|
||||
const posterElementRef = useRef<HTMLImageElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const savedTimeRef = useRef(0);
|
||||
const frameCleanupRef = useRef<(() => void) | null>(null);
|
||||
const playbackRequestIdRef = useRef(0);
|
||||
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
|
||||
const [isHoveredState, setIsHoveredState] = useState(false);
|
||||
const [isFocusWithin, setIsFocusWithin] = useState(false);
|
||||
const {ref: visibilityRef, isNearViewport} = useNearViewport<HTMLDivElement>({rememberKey: videoProxyURL});
|
||||
const setContainerRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
containerRef.current = node;
|
||||
setContainerElement(node);
|
||||
visibilityRef(node);
|
||||
},
|
||||
[visibilityRef],
|
||||
);
|
||||
const {shouldBlur, gateReason, canReveal, reveal: revealSensitiveMedia} = useMatureMedia(nsfw, channelId);
|
||||
const shouldLoadMedia = isNearViewport && !shouldBlur;
|
||||
const posterURL = useMemo(() => buildMediaProxyURL(videoProxyURL, {format: 'webp'}), [videoProxyURL]);
|
||||
const shouldLoadVideo = isNearViewport && !shouldBlur;
|
||||
const {dimensions} = mediaCalculator.calculate({width: naturalWidth, height: naturalHeight}, {forceScale: true});
|
||||
const posterSource = thumbnailProxyURL && thumbnailProxyURL.length > 0 ? thumbnailProxyURL : videoProxyURL;
|
||||
const posterURL = useMemo(
|
||||
() =>
|
||||
buildFittedStaticGifPreviewURL(
|
||||
stripMediaProxyParams(posterSource),
|
||||
Math.max(1, Math.round(dimensions.width * 2)),
|
||||
Math.max(1, Math.round(dimensions.height * 2)),
|
||||
),
|
||||
[dimensions.height, dimensions.width, posterSource],
|
||||
);
|
||||
const posterCachedForPaint = ImageCacheUtils.hasImage(posterURL);
|
||||
const shouldLoadPoster = (isNearViewport || posterCachedForPaint) && !shouldBlur;
|
||||
const {
|
||||
loaded,
|
||||
error,
|
||||
@@ -359,14 +442,106 @@ export const EmbedGifv: FC<
|
||||
ref: mediaRef,
|
||||
onLoad: handleMediaLoad,
|
||||
onError: handleMediaError,
|
||||
} = useMediaLoading(videoProxyURL, placeholder, {enabled: shouldLoadMedia});
|
||||
} = useMediaLoading(posterURL, placeholder, {enabled: shouldLoadPoster});
|
||||
const renderSourceKey = `${videoProxyURL}\n${posterURL}\n${shouldLoadVideo ? 'active' : 'inactive'}`;
|
||||
const [renderState, dispatchRenderEvent] = useReducer(
|
||||
reduceGIFVRenderState,
|
||||
{
|
||||
sourceKey: renderSourceKey,
|
||||
posterCached: ImageCacheUtils.hasImage(posterURL),
|
||||
},
|
||||
createInitialGIFVRenderState,
|
||||
);
|
||||
const currentRenderSourceKeyRef = useRef(renderSourceKey);
|
||||
const setVideoRef = useCallback(
|
||||
(node: HTMLVideoElement | null) => {
|
||||
if (videoRef.current !== node && frameCleanupRef.current != null) {
|
||||
frameCleanupRef.current();
|
||||
frameCleanupRef.current = null;
|
||||
}
|
||||
videoRef.current = node;
|
||||
if (node == null) return;
|
||||
if (currentRenderSourceKeyRef.current !== renderSourceKey) return;
|
||||
if (!mediaElementHasSource(node, videoProxyURL)) return;
|
||||
if (node.readyState < 2) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.VIDEO_READY});
|
||||
},
|
||||
[renderSourceKey, videoProxyURL],
|
||||
);
|
||||
const setPosterRef = useCallback(
|
||||
(node: HTMLImageElement | null) => {
|
||||
posterElementRef.current = node;
|
||||
mediaRef(node);
|
||||
},
|
||||
[mediaRef],
|
||||
);
|
||||
useLayoutEffect(() => {
|
||||
currentRenderSourceKeyRef.current = renderSourceKey;
|
||||
if (frameCleanupRef.current != null) {
|
||||
frameCleanupRef.current();
|
||||
frameCleanupRef.current = null;
|
||||
}
|
||||
dispatchRenderEvent({
|
||||
type: GIFVRenderEventType.SOURCE_CHANGED,
|
||||
sourceKey: renderSourceKey,
|
||||
posterCached: ImageCacheUtils.hasImage(posterURL),
|
||||
});
|
||||
if (!shouldLoadVideo) return;
|
||||
const video = videoRef.current;
|
||||
if (video == null) return;
|
||||
if (!mediaElementHasSource(video, videoProxyURL)) return;
|
||||
if (video.readyState < 2) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.VIDEO_READY});
|
||||
}, [posterURL, renderSourceKey, shouldLoadVideo, videoProxyURL]);
|
||||
const renderStateIsCurrent = renderState.sourceKey === renderSourceKey;
|
||||
const currentRenderState = renderStateIsCurrent
|
||||
? renderState
|
||||
: createInitialGIFVRenderState({
|
||||
sourceKey: renderSourceKey,
|
||||
posterCached: ImageCacheUtils.hasImage(posterURL),
|
||||
});
|
||||
useEffect(() => {
|
||||
if (!shouldLoadPoster) return;
|
||||
if (loaded) {
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.POSTER_LOADED});
|
||||
return;
|
||||
}
|
||||
if (error) {
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.POSTER_FAILED});
|
||||
return;
|
||||
}
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.POSTER_LOAD_STARTED});
|
||||
}, [error, loaded, shouldLoadPoster]);
|
||||
useEffect(() => {
|
||||
if (shouldLoadVideo) dispatchRenderEvent({type: GIFVRenderEventType.VIDEO_LOAD_STARTED});
|
||||
}, [renderSourceKey, shouldLoadVideo]);
|
||||
useEffect(() => {
|
||||
if (frameCleanupRef.current != null) {
|
||||
frameCleanupRef.current();
|
||||
frameCleanupRef.current = null;
|
||||
}
|
||||
if (currentRenderState.video === GIFVVideoState.PRESENTED) return;
|
||||
if (currentRenderState.playback !== GIFVPlaybackState.PLAYING) return;
|
||||
const video = videoRef.current;
|
||||
if (video == null || video.readyState < 2) return;
|
||||
const sourceKey = renderSourceKey;
|
||||
const cancelFrameWatch = watchPresentedVideoFrame(video, () => {
|
||||
if (currentRenderSourceKeyRef.current !== sourceKey) return;
|
||||
if (videoRef.current !== video) return;
|
||||
if (!mediaElementHasSource(video, videoProxyURL)) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.VIDEO_FRAME_PRESENTED});
|
||||
});
|
||||
frameCleanupRef.current = cancelFrameWatch;
|
||||
return () => {
|
||||
cancelFrameWatch();
|
||||
if (frameCleanupRef.current === cancelFrameWatch) frameCleanupRef.current = null;
|
||||
};
|
||||
}, [currentRenderState.playback, currentRenderState.video, renderSourceKey, videoProxyURL]);
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (frameCleanupRef.current != null) frameCleanupRef.current();
|
||||
};
|
||||
}, []);
|
||||
const defaultName = deriveDefaultNameFromMessage({
|
||||
message,
|
||||
attachmentId,
|
||||
@@ -456,91 +631,152 @@ export const EmbedGifv: FC<
|
||||
snapshotIndex,
|
||||
],
|
||||
);
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
const handlePlaying = () => {
|
||||
if (video.hasAttribute('poster')) {
|
||||
video.removeAttribute('poster');
|
||||
}
|
||||
};
|
||||
video.addEventListener('playing', handlePlaying);
|
||||
return () => {
|
||||
video.removeEventListener('playing', handlePlaying);
|
||||
};
|
||||
}, []);
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (!video) return;
|
||||
if (!shouldLoadMedia) {
|
||||
video.autoplay = false;
|
||||
safePause(video);
|
||||
return;
|
||||
}
|
||||
if (isMediaViewerOpen) {
|
||||
video.autoplay = false;
|
||||
safePause(video);
|
||||
return;
|
||||
}
|
||||
const shouldPlay = gifAutoPlay && animatedMediaPlaybackAllowed;
|
||||
if (shouldPlay) {
|
||||
video.autoplay = true;
|
||||
void safePlay(video);
|
||||
} else {
|
||||
video.autoplay = false;
|
||||
safePause(video);
|
||||
}
|
||||
}, [animatedMediaPlaybackAllowed, videoConfig, gifAutoPlay, isMediaViewerOpen, shouldLoadMedia]);
|
||||
useEffect(() => {
|
||||
if (!shouldLoadMedia || gifAutoPlay || isMediaViewerOpen) return;
|
||||
const video = videoRef.current;
|
||||
const container = containerRef.current;
|
||||
if (!video || !container) return;
|
||||
let isHovered = false;
|
||||
const handleMouseEnter = () => {
|
||||
isHovered = true;
|
||||
if (!getAnimatedMediaPlaybackAllowed()) return;
|
||||
const target = savedTimeRef.current;
|
||||
if (Number.isFinite(target) && Math.abs(video.currentTime - target) > 0.01) {
|
||||
try {
|
||||
video.currentTime = target;
|
||||
} catch {}
|
||||
}
|
||||
void safePlay(video);
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
isHovered = false;
|
||||
if (Number.isFinite(video.currentTime)) {
|
||||
savedTimeRef.current = video.currentTime;
|
||||
}
|
||||
safePause(video);
|
||||
};
|
||||
const handlePlaybackAllowedChange = () => {
|
||||
if (getAnimatedMediaPlaybackAllowed()) {
|
||||
if (!isHovered) return;
|
||||
const target = savedTimeRef.current;
|
||||
if (Number.isFinite(target) && Math.abs(video.currentTime - target) > 0.01) {
|
||||
try {
|
||||
video.currentTime = target;
|
||||
} catch {}
|
||||
const handlePosterLoad = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
if (currentRenderSourceKeyRef.current !== renderSourceKey) return;
|
||||
if (posterElementRef.current !== event.currentTarget) return;
|
||||
if (!mediaElementHasSource(event.currentTarget, posterURL)) return;
|
||||
handleMediaLoad(event);
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.POSTER_LOADED});
|
||||
},
|
||||
[handleMediaLoad, posterURL, renderSourceKey],
|
||||
);
|
||||
const handlePosterError = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
if (currentRenderSourceKeyRef.current !== renderSourceKey) return;
|
||||
if (posterElementRef.current !== event.currentTarget) return;
|
||||
if (!mediaElementHasSource(event.currentTarget, posterURL)) return;
|
||||
handleMediaError(event);
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.POSTER_FAILED});
|
||||
},
|
||||
[handleMediaError, posterURL, renderSourceKey],
|
||||
);
|
||||
const handleVideoReady = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||
if (currentRenderSourceKeyRef.current !== renderSourceKey) return;
|
||||
if (videoRef.current !== event.currentTarget) return;
|
||||
if (!mediaElementHasSource(event.currentTarget, videoProxyURL)) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.VIDEO_READY});
|
||||
},
|
||||
[renderSourceKey, videoProxyURL],
|
||||
);
|
||||
const handleVideoPlaying = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||
if (currentRenderSourceKeyRef.current !== renderSourceKey) return;
|
||||
if (videoRef.current !== event.currentTarget) return;
|
||||
if (!mediaElementHasSource(event.currentTarget, videoProxyURL)) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.PLAYING});
|
||||
},
|
||||
[renderSourceKey, videoProxyURL],
|
||||
);
|
||||
const handleVideoPause = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||
if (currentRenderSourceKeyRef.current !== renderSourceKey) return;
|
||||
if (videoRef.current !== event.currentTarget) return;
|
||||
if (!mediaElementHasSource(event.currentTarget, videoProxyURL)) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.PLAYBACK_PAUSED});
|
||||
},
|
||||
[renderSourceKey, videoProxyURL],
|
||||
);
|
||||
const handleVideoError = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||
if (currentRenderSourceKeyRef.current !== renderSourceKey) return;
|
||||
if (videoRef.current !== event.currentTarget) return;
|
||||
if (!mediaElementHasSource(event.currentTarget, videoProxyURL)) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.VIDEO_FAILED});
|
||||
},
|
||||
[renderSourceKey, videoProxyURL],
|
||||
);
|
||||
const requestVideoPlayback = useCallback(
|
||||
(video: HTMLVideoElement) => {
|
||||
const sourceKey = renderSourceKey;
|
||||
const requestId = playbackRequestIdRef.current;
|
||||
if (currentRenderSourceKeyRef.current !== sourceKey) return;
|
||||
if (videoRef.current !== video) return;
|
||||
if (!mediaElementHasSource(video, videoProxyURL)) return;
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.PLAY_REQUESTED});
|
||||
if (!getAnimatedMediaPlaybackAllowed()) {
|
||||
if (currentRenderSourceKeyRef.current === sourceKey) {
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.PLAYBACK_BLOCKED});
|
||||
}
|
||||
void safePlay(video);
|
||||
return;
|
||||
}
|
||||
if (Number.isFinite(video.currentTime)) {
|
||||
savedTimeRef.current = video.currentTime;
|
||||
}
|
||||
safePause(video);
|
||||
const playPromise = safePlay(video);
|
||||
void playPromise.then(() => {
|
||||
if (playbackRequestIdRef.current !== requestId) return;
|
||||
if (currentRenderSourceKeyRef.current !== sourceKey) return;
|
||||
if (videoRef.current !== video) return;
|
||||
if (!mediaElementHasSource(video, videoProxyURL)) return;
|
||||
if (video.paused) dispatchRenderEvent({type: GIFVRenderEventType.PLAYBACK_BLOCKED});
|
||||
});
|
||||
},
|
||||
[renderSourceKey, videoProxyURL],
|
||||
);
|
||||
useEffect(() => {
|
||||
const container = containerElement;
|
||||
if (container == null) return;
|
||||
const saveCurrentTime = () => {
|
||||
const video = videoRef.current;
|
||||
if (video != null && Number.isFinite(video.currentTime)) savedTimeRef.current = video.currentTime;
|
||||
};
|
||||
const handleMouseEnter = () => {
|
||||
setIsHoveredState(true);
|
||||
};
|
||||
const handleMouseLeave = () => {
|
||||
setIsHoveredState(false);
|
||||
saveCurrentTime();
|
||||
};
|
||||
const handleFocusIn = () => {
|
||||
setIsFocusWithin(true);
|
||||
};
|
||||
const handleFocusOut = (event: FocusEvent) => {
|
||||
const nextTarget = event.relatedTarget;
|
||||
if (nextTarget != null && container.contains(nextTarget as Node)) return;
|
||||
setIsFocusWithin(false);
|
||||
saveCurrentTime();
|
||||
};
|
||||
container.addEventListener('mouseenter', handleMouseEnter);
|
||||
container.addEventListener('mouseleave', handleMouseLeave);
|
||||
const unsubscribe = subscribeAnimatedMediaPlaybackChange(handlePlaybackAllowedChange);
|
||||
container.addEventListener('focusin', handleFocusIn);
|
||||
container.addEventListener('focusout', handleFocusOut);
|
||||
return () => {
|
||||
container.removeEventListener('mouseenter', handleMouseEnter);
|
||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||
unsubscribe();
|
||||
container.removeEventListener('focusin', handleFocusIn);
|
||||
container.removeEventListener('focusout', handleFocusOut);
|
||||
};
|
||||
}, [gifAutoPlay, isMediaViewerOpen, shouldLoadMedia]);
|
||||
}, [containerElement]);
|
||||
useEffect(() => {
|
||||
const video = videoRef.current;
|
||||
if (video == null) return;
|
||||
playbackRequestIdRef.current += 1;
|
||||
const playbackWanted = shouldLoadVideo && !isMediaViewerOpen && (gifAutoPlay || isHoveredState || isFocusWithin);
|
||||
if (!playbackWanted || !animatedMediaPlaybackAllowed) {
|
||||
video.autoplay = false;
|
||||
if (playbackWanted && currentRenderSourceKeyRef.current === renderSourceKey) {
|
||||
dispatchRenderEvent({type: GIFVRenderEventType.PLAYBACK_BLOCKED});
|
||||
}
|
||||
safePause(video);
|
||||
return;
|
||||
}
|
||||
video.autoplay = true;
|
||||
const target = savedTimeRef.current;
|
||||
if (Number.isFinite(target) && Math.abs(video.currentTime - target) > 0.01) {
|
||||
try {
|
||||
video.currentTime = target;
|
||||
} catch {}
|
||||
}
|
||||
requestVideoPlayback(video);
|
||||
}, [
|
||||
animatedMediaPlaybackAllowed,
|
||||
gifAutoPlay,
|
||||
isFocusWithin,
|
||||
isHoveredState,
|
||||
isMediaViewerOpen,
|
||||
renderSourceKey,
|
||||
requestVideoPlayback,
|
||||
shouldLoadVideo,
|
||||
]);
|
||||
if (shouldBlur) {
|
||||
const {style} = mediaCalculator.calculate({width: naturalWidth, height: naturalHeight}, {forceScale: true});
|
||||
const {width: _width, height: _height, ...styleWithoutDimensions} = style;
|
||||
@@ -577,10 +813,7 @@ export const EmbedGifv: FC<
|
||||
</div>
|
||||
);
|
||||
}
|
||||
const {style, dimensions} = mediaCalculator.calculate(
|
||||
{width: naturalWidth, height: naturalHeight},
|
||||
{forceScale: true},
|
||||
);
|
||||
const {style} = mediaCalculator.calculate({width: naturalWidth, height: naturalHeight}, {forceScale: true});
|
||||
const {
|
||||
showFavoriteButton,
|
||||
showDownloadButton: _showDownloadButton,
|
||||
@@ -601,7 +834,28 @@ export const EmbedGifv: FC<
|
||||
width: remFromPx(dimensions.width),
|
||||
...(aspectRatio ? {aspectRatio} : {}),
|
||||
} as React.CSSProperties;
|
||||
const effectivePreload = shouldLoadMedia ? (videoConfig?.preload ?? (gifAutoPlay ? 'auto' : 'metadata')) : 'none';
|
||||
let effectivePreload: 'none' | 'metadata' | 'auto' = 'none';
|
||||
if (shouldLoadVideo) {
|
||||
if (videoConfig && videoConfig.preload !== undefined) {
|
||||
effectivePreload = videoConfig.preload;
|
||||
} else {
|
||||
effectivePreload = gifAutoPlay ? 'auto' : 'metadata';
|
||||
}
|
||||
}
|
||||
const renderLayers = getGIFVRenderLayers(currentRenderState);
|
||||
const posterReady = loaded && !error;
|
||||
const showPoster = shouldLoadPoster && posterReady && renderLayers.showPoster;
|
||||
const showVideo = shouldLoadVideo && renderLayers.showVideo;
|
||||
let videoControls = false;
|
||||
let videoPlaysInline = true;
|
||||
let videoLoop = true;
|
||||
let videoMuted = true;
|
||||
if (videoConfig) {
|
||||
if (videoConfig.controls !== undefined) videoControls = videoConfig.controls;
|
||||
if (videoConfig.playsInline !== undefined) videoPlaysInline = videoConfig.playsInline;
|
||||
if (videoConfig.loop !== undefined) videoLoop = videoConfig.loop;
|
||||
if (videoConfig.muted !== undefined) videoMuted = videoConfig.muted;
|
||||
}
|
||||
return (
|
||||
<MediaContainer
|
||||
ref={setContainerRef}
|
||||
@@ -621,11 +875,6 @@ export const EmbedGifv: FC<
|
||||
data-flx="channel.embeds.media.embed-gifv.media-container.context-menu"
|
||||
>
|
||||
{showGifIndicator && <GifIndicator data-flx="channel.embeds.media.embed-gifv.gif-indicator" />}
|
||||
{providerName === 'KLIPY' && (
|
||||
<div className={styles.klipyWatermark} data-flx="channel.embeds.media.embed-gifv.klipy-watermark">
|
||||
<KlipyWatermarkSvg data-flx="channel.embeds.media.embed-gifv.klipy-watermark-svg" />
|
||||
</div>
|
||||
)}
|
||||
<ImagePreviewHandler
|
||||
src={videoProxyURL}
|
||||
originalSrc={embedURL}
|
||||
@@ -636,7 +885,7 @@ export const EmbedGifv: FC<
|
||||
data-flx="channel.embeds.media.embed-gifv.image-preview-handler.gifv"
|
||||
>
|
||||
<div className={styles.videoWrapper} data-flx="channel.embeds.media.embed-gifv.video-wrapper">
|
||||
{(!loaded || error) && thumbHashURL && (
|
||||
{(renderLayers.showPlaceholder || !loaded || error) && thumbHashURL && (
|
||||
<img
|
||||
src={thumbHashURL}
|
||||
className={styles.thumbHashPlaceholder}
|
||||
@@ -644,29 +893,48 @@ export const EmbedGifv: FC<
|
||||
data-flx="channel.embeds.media.embed-gifv.thumb-hash-placeholder--2"
|
||||
/>
|
||||
)}
|
||||
<motion.img
|
||||
ref={setPosterRef}
|
||||
className={styles.posterElement}
|
||||
alt={i18n._(LOADING_PLACEHOLDER_DESCRIPTOR)}
|
||||
src={shouldLoadPoster ? posterURL : undefined}
|
||||
loading="lazy"
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
tabIndex={-1}
|
||||
onLoad={handlePosterLoad}
|
||||
onError={handlePosterError}
|
||||
initial={{opacity: cached || cachedOnMount ? 1 : 0}}
|
||||
animate={{opacity: showPoster ? 1 : 0}}
|
||||
transition={{
|
||||
duration:
|
||||
cached || cachedOnMount || Accessibility.useReducedMotion ? 0 : EMBED_MEDIA_FADE_DURATION_SECONDS,
|
||||
}}
|
||||
data-flx="channel.embeds.media.embed-gifv.poster-element"
|
||||
/>
|
||||
<motion.video
|
||||
className={styles.videoElement}
|
||||
controls={videoConfig?.controls ?? false}
|
||||
playsInline={videoConfig?.playsInline ?? true}
|
||||
loop={videoConfig?.loop ?? true}
|
||||
muted={videoConfig?.muted ?? true}
|
||||
poster={shouldLoadMedia ? posterURL : thumbHashURL}
|
||||
controls={videoControls}
|
||||
playsInline={videoPlaysInline}
|
||||
loop={videoLoop}
|
||||
muted={videoMuted}
|
||||
poster={shouldLoadPoster ? posterURL : thumbHashURL}
|
||||
preload={effectivePreload}
|
||||
src={shouldLoadMedia ? videoProxyURL : undefined}
|
||||
src={shouldLoadVideo ? videoProxyURL : undefined}
|
||||
ref={setVideoRef}
|
||||
aria-label={i18n._(ANIMATED_GIF_VIDEO_DESCRIPTOR)}
|
||||
data-embed-media="gifv"
|
||||
tabIndex={-1}
|
||||
width={dimensions.width}
|
||||
height={dimensions.height}
|
||||
onLoadedData={handleMediaLoad}
|
||||
onError={handleMediaError}
|
||||
initial={{opacity: cached || cachedOnMount ? 1 : 0}}
|
||||
animate={{opacity: !loaded && !error ? 0 : 1}}
|
||||
transition={{
|
||||
duration:
|
||||
cached || cachedOnMount || Accessibility.useReducedMotion ? 0 : EMBED_MEDIA_FADE_DURATION_SECONDS,
|
||||
}}
|
||||
onLoadedData={handleVideoReady}
|
||||
onCanPlay={handleVideoReady}
|
||||
onPlaying={handleVideoPlaying}
|
||||
onPause={handleVideoPause}
|
||||
onError={handleVideoError}
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: showVideo ? 1 : 0}}
|
||||
transition={{duration: Accessibility.useReducedMotion ? 0 : EMBED_MEDIA_FADE_DURATION_SECONDS}}
|
||||
data-flx="channel.embeds.media.embed-gifv.video-element"
|
||||
/>
|
||||
</div>
|
||||
@@ -713,7 +981,9 @@ export const EmbedGif: FC<
|
||||
const isMobile = MobileLayout.enabled;
|
||||
const mediaCalculator = useEmbedMediaCalculator(layoutConstraints);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
const [containerElement, setContainerElement] = useState<HTMLDivElement | null>(null);
|
||||
const imgRef = useRef<HTMLImageElement>(null);
|
||||
const animatedImageRef = useRef<HTMLImageElement>(null);
|
||||
const freezeCanvasRef = useRef<HTMLCanvasElement>(null);
|
||||
const {ref: visibilityRef, isNearViewport} = useNearViewport<HTMLDivElement>({
|
||||
disabled: !isMobile,
|
||||
@@ -722,6 +992,7 @@ export const EmbedGif: FC<
|
||||
const setContainerRef = useCallback(
|
||||
(node: HTMLDivElement | null) => {
|
||||
containerRef.current = node;
|
||||
setContainerElement(node);
|
||||
visibilityRef(node);
|
||||
},
|
||||
[visibilityRef],
|
||||
@@ -733,7 +1004,9 @@ export const EmbedGif: FC<
|
||||
const baseProxyURL = stripMediaProxyParams(proxyURL);
|
||||
const animatedTargetWidth = Math.min(naturalWidth, Math.round(displayWidth * 2));
|
||||
const animatedTargetHeight = Math.min(naturalHeight, Math.round(displayHeight * 2));
|
||||
const shouldResizeAnimated = animatedTargetWidth < naturalWidth || animatedTargetHeight < naturalHeight;
|
||||
const naturalSizeIsKnown = naturalWidth > 0 && naturalHeight > 0;
|
||||
const shouldResizeAnimated =
|
||||
naturalSizeIsKnown && (animatedTargetWidth < naturalWidth || animatedTargetHeight < naturalHeight);
|
||||
const optimizedAnimatedURL = buildFittedAnimatedImageProxyURL(
|
||||
baseProxyURL,
|
||||
shouldResizeAnimated ? animatedTargetWidth : undefined,
|
||||
@@ -744,6 +1017,7 @@ export const EmbedGif: FC<
|
||||
Math.round(displayWidth * 2),
|
||||
Math.round(displayHeight * 2),
|
||||
);
|
||||
const animatedImageSourceRef = useRef(optimizedAnimatedURL);
|
||||
const {shouldBlur, gateReason, canReveal, reveal: revealSensitiveMedia} = useMatureMedia(nsfw, channelId);
|
||||
const shouldLoadMedia = isNearViewport && !shouldBlur;
|
||||
const {
|
||||
@@ -769,6 +1043,11 @@ export const EmbedGif: FC<
|
||||
const isHoveredRef = useRef(false);
|
||||
const [hasStartedAnimating, setHasStartedAnimating] = useState(gifAutoPlay);
|
||||
const [isHoveredState, setIsHoveredState] = useState(false);
|
||||
const [renderedAnimatedURL, setRenderedAnimatedURL] = useState('');
|
||||
const animatedPreloadStateRef = useRef<AnimatedImagePreloadState>({
|
||||
url: optimizedAnimatedURL,
|
||||
status: ImageCacheUtils.hasImage(optimizedAnimatedURL) ? 'ready' : 'idle',
|
||||
});
|
||||
const [decoderRequested, setDecoderRequested] = useState(() => getAnimatedMediaPlaybackAllowed());
|
||||
const shouldUseDecoder = shouldLoadMedia && hasStartedAnimating;
|
||||
const decoderPlaying =
|
||||
@@ -781,7 +1060,53 @@ export const EmbedGif: FC<
|
||||
});
|
||||
const useDecoder =
|
||||
shouldUseDecoder && decoderRequested && decoderState.supported && hasStartedAnimating && !decoderState.error;
|
||||
const shouldRenderFreezeFrame = !useDecoder && (gifAutoPlay || hasStartedAnimating);
|
||||
const decoderCanvasVisible = useDecoder && decoderState.loaded;
|
||||
const shouldRenderFreezeFrame = !decoderCanvasVisible && (gifAutoPlay || hasStartedAnimating);
|
||||
const setAnimatedImageRef = useCallback(
|
||||
(node: HTMLImageElement | null) => {
|
||||
animatedImageRef.current = node;
|
||||
if (animatedImageSourceRef.current !== optimizedAnimatedURL) return;
|
||||
if (completeImageHasSource(node, optimizedAnimatedURL)) setRenderedAnimatedURL(optimizedAnimatedURL);
|
||||
},
|
||||
[optimizedAnimatedURL],
|
||||
);
|
||||
useLayoutEffect(() => {
|
||||
animatedImageSourceRef.current = optimizedAnimatedURL;
|
||||
setRenderedAnimatedURL('');
|
||||
if (completeImageHasSource(animatedImageRef.current, optimizedAnimatedURL)) {
|
||||
setRenderedAnimatedURL(optimizedAnimatedURL);
|
||||
}
|
||||
}, [optimizedAnimatedURL]);
|
||||
useLayoutEffect(() => {
|
||||
const nextState: AnimatedImagePreloadState = {
|
||||
url: optimizedAnimatedURL,
|
||||
status: ImageCacheUtils.hasImage(optimizedAnimatedURL) ? 'ready' : 'idle',
|
||||
};
|
||||
animatedPreloadStateRef.current = nextState;
|
||||
}, [optimizedAnimatedURL]);
|
||||
const handleAnimatedImageLoad = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
const element = event.currentTarget;
|
||||
if (animatedImageSourceRef.current !== optimizedAnimatedURL) return;
|
||||
if (animatedImageRef.current !== element) return;
|
||||
if (!completeImageHasSource(element, optimizedAnimatedURL)) return;
|
||||
ImageCacheUtils.rememberImage(optimizedAnimatedURL, element);
|
||||
animatedPreloadStateRef.current = {url: optimizedAnimatedURL, status: 'ready'};
|
||||
setRenderedAnimatedURL(optimizedAnimatedURL);
|
||||
},
|
||||
[optimizedAnimatedURL],
|
||||
);
|
||||
const handleAnimatedImageError = useCallback(
|
||||
(event: React.SyntheticEvent<HTMLImageElement>) => {
|
||||
if (animatedImageSourceRef.current !== optimizedAnimatedURL) return;
|
||||
if (animatedImageRef.current !== event.currentTarget) return;
|
||||
if (!mediaElementHasSource(event.currentTarget, optimizedAnimatedURL)) return;
|
||||
setRenderedAnimatedURL('');
|
||||
ImageCacheUtils.forgetImage(optimizedAnimatedURL);
|
||||
animatedPreloadStateRef.current = {url: optimizedAnimatedURL, status: 'failed'};
|
||||
},
|
||||
[optimizedAnimatedURL],
|
||||
);
|
||||
const defaultName = deriveDefaultNameFromMessage({
|
||||
message,
|
||||
attachmentId,
|
||||
@@ -880,39 +1205,81 @@ export const EmbedGif: FC<
|
||||
}
|
||||
}, [animatedMediaPlaybackAllowed, shouldUseDecoder]);
|
||||
useEffect(() => {
|
||||
if (!shouldLoadMedia || gifAutoPlay || !animatedMediaPlaybackAllowed) return;
|
||||
if (!optimizedAnimatedURL) return;
|
||||
const preloader = new Image();
|
||||
preloader.src = optimizedAnimatedURL;
|
||||
}, [animatedMediaPlaybackAllowed, gifAutoPlay, optimizedAnimatedURL, shouldLoadMedia]);
|
||||
const shouldPreloadAnimated = shouldLoadMedia && animatedMediaPlaybackAllowed && optimizedAnimatedURL.length > 0;
|
||||
if (!shouldPreloadAnimated) return;
|
||||
const preloadState = animatedPreloadStateRef.current;
|
||||
if (preloadState.url !== optimizedAnimatedURL || preloadState.status !== 'idle') return;
|
||||
const loadingState: AnimatedImagePreloadState = {url: optimizedAnimatedURL, status: 'loading'};
|
||||
animatedPreloadStateRef.current = loadingState;
|
||||
let active = true;
|
||||
const cleanup = ImageCacheUtils.loadImage(
|
||||
optimizedAnimatedURL,
|
||||
() => {
|
||||
if (!active) return;
|
||||
const readyState: AnimatedImagePreloadState = {url: optimizedAnimatedURL, status: 'ready'};
|
||||
animatedPreloadStateRef.current = readyState;
|
||||
},
|
||||
() => {
|
||||
if (!active) return;
|
||||
const failedState: AnimatedImagePreloadState = {url: optimizedAnimatedURL, status: 'failed'};
|
||||
animatedPreloadStateRef.current = failedState;
|
||||
},
|
||||
);
|
||||
return () => {
|
||||
active = false;
|
||||
cleanup();
|
||||
if (animatedPreloadStateRef.current !== loadingState) return;
|
||||
const idleState: AnimatedImagePreloadState = {url: optimizedAnimatedURL, status: 'idle'};
|
||||
animatedPreloadStateRef.current = idleState;
|
||||
};
|
||||
}, [animatedMediaPlaybackAllowed, optimizedAnimatedURL, shouldLoadMedia]);
|
||||
const showFreezeFrame = useCallback(() => {
|
||||
const img = imgRef.current;
|
||||
const canvas = freezeCanvasRef.current;
|
||||
if (!img || !canvas) return;
|
||||
const sourceWidth = img.naturalWidth || img.width;
|
||||
const sourceHeight = img.naturalHeight || img.height;
|
||||
if (sourceWidth === 0 || sourceHeight === 0) return;
|
||||
if (canvas == null) return;
|
||||
canvas.removeAttribute('data-frozen');
|
||||
const animatedImage = animatedImageRef.current;
|
||||
const staticImage = imgRef.current;
|
||||
let image: HTMLImageElement;
|
||||
if (completeImageHasSource(animatedImage, optimizedAnimatedURL)) {
|
||||
image = animatedImage;
|
||||
} else if (completeImageHasSource(staticImage, optimizedStaticURL)) {
|
||||
image = staticImage;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
const sourceWidth = image.naturalWidth;
|
||||
const sourceHeight = image.naturalHeight;
|
||||
if (canvas.width !== sourceWidth) canvas.width = sourceWidth;
|
||||
if (canvas.height !== sourceHeight) canvas.height = sourceHeight;
|
||||
const ctx = canvas.getContext('2d');
|
||||
if (!ctx) return;
|
||||
if (ctx == null) return;
|
||||
try {
|
||||
ctx.clearRect(0, 0, sourceWidth, sourceHeight);
|
||||
ctx.drawImage(img, 0, 0, sourceWidth, sourceHeight);
|
||||
ctx.drawImage(image, 0, 0, sourceWidth, sourceHeight);
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
canvas.dataset.frozen = 'true';
|
||||
}, []);
|
||||
}, [optimizedAnimatedURL, optimizedStaticURL]);
|
||||
const hideFreezeFrame = useCallback(() => {
|
||||
const canvas = freezeCanvasRef.current;
|
||||
if (!canvas) return;
|
||||
canvas.dataset.frozen = 'false';
|
||||
if (canvas == null) return;
|
||||
canvas.removeAttribute('data-frozen');
|
||||
}, []);
|
||||
const resetFreezeFrame = useCallback(() => {
|
||||
const canvas = freezeCanvasRef.current;
|
||||
if (canvas == null) return;
|
||||
canvas.removeAttribute('data-frozen');
|
||||
canvas.width = 0;
|
||||
canvas.height = 0;
|
||||
}, []);
|
||||
useLayoutEffect(() => {
|
||||
resetFreezeFrame();
|
||||
}, [optimizedAnimatedURL, optimizedStaticURL, resetFreezeFrame]);
|
||||
useEffect(() => {
|
||||
if (!shouldLoadMedia || gifAutoPlay) return;
|
||||
const container = containerRef.current;
|
||||
if (!container) return;
|
||||
const container = containerElement;
|
||||
if (container == null) return;
|
||||
const handleMouseEnter = () => {
|
||||
isHoveredRef.current = true;
|
||||
setIsHoveredState(true);
|
||||
@@ -934,7 +1301,7 @@ export const EmbedGif: FC<
|
||||
container.removeEventListener('mouseenter', handleMouseEnter);
|
||||
container.removeEventListener('mouseleave', handleMouseLeave);
|
||||
};
|
||||
}, [gifAutoPlay, hasStartedAnimating, hideFreezeFrame, shouldLoadMedia, showFreezeFrame]);
|
||||
}, [containerElement, gifAutoPlay, hasStartedAnimating, hideFreezeFrame, shouldLoadMedia, showFreezeFrame]);
|
||||
useEffect(() => {
|
||||
if (!shouldLoadMedia || gifAutoPlay) return;
|
||||
if (!animatedMediaPlaybackAllowed) {
|
||||
@@ -1041,8 +1408,9 @@ export const EmbedGif: FC<
|
||||
const shouldUseAnimatedImage =
|
||||
shouldLoadMedia &&
|
||||
animatedMediaPlaybackAllowed &&
|
||||
!useDecoder &&
|
||||
!decoderCanvasVisible &&
|
||||
(gifAutoPlay || (hasStartedAnimating && isHoveredState));
|
||||
const animatedImageVisible = shouldUseAnimatedImage && renderedAnimatedURL === optimizedAnimatedURL;
|
||||
return (
|
||||
<MediaContainer
|
||||
ref={setContainerRef}
|
||||
@@ -1090,7 +1458,7 @@ export const EmbedGif: FC<
|
||||
<motion.img
|
||||
ref={setImgRef}
|
||||
alt={i18n._(ANIMATED_GIF_DESCRIPTOR)}
|
||||
src={shouldLoadMedia ? (shouldUseAnimatedImage ? optimizedAnimatedURL : optimizedStaticURL) : undefined}
|
||||
src={shouldLoadMedia ? optimizedStaticURL : undefined}
|
||||
className={styles.videoElement}
|
||||
data-embed-media="gif"
|
||||
loading={isMobile ? 'lazy' : 'eager'}
|
||||
@@ -1100,21 +1468,41 @@ export const EmbedGif: FC<
|
||||
onLoad={handleImageLoad}
|
||||
onError={handleImageError}
|
||||
initial={{opacity: cached || cachedOnMount ? 1 : 0}}
|
||||
animate={{
|
||||
opacity: (!loaded && !error) || (useDecoder && decoderState.loaded) ? 0 : 1,
|
||||
}}
|
||||
animate={{opacity: loaded && !decoderCanvasVisible ? 1 : 0}}
|
||||
transition={{
|
||||
duration:
|
||||
cached || cachedOnMount || Accessibility.useReducedMotion ? 0 : EMBED_MEDIA_FADE_DURATION_SECONDS,
|
||||
}}
|
||||
data-flx="channel.embeds.media.embed-gifv.embed-gif.video-element"
|
||||
/>
|
||||
{shouldUseAnimatedImage && (
|
||||
<motion.img
|
||||
ref={setAnimatedImageRef}
|
||||
alt={i18n._(ANIMATED_GIF_DESCRIPTOR)}
|
||||
src={optimizedAnimatedURL}
|
||||
className={styles.posterElement}
|
||||
data-embed-media="gif-animated"
|
||||
loading={isMobile ? 'lazy' : 'eager'}
|
||||
tabIndex={-1}
|
||||
width={renderedDimensions.width}
|
||||
height={renderedDimensions.height}
|
||||
onLoad={handleAnimatedImageLoad}
|
||||
onError={handleAnimatedImageError}
|
||||
initial={{opacity: 0}}
|
||||
animate={{opacity: animatedImageVisible ? 1 : 0}}
|
||||
transition={{duration: Accessibility.useReducedMotion ? 0 : EMBED_MEDIA_FADE_DURATION_SECONDS}}
|
||||
data-flx="channel.embeds.media.embed-gifv.embed-gif.animated-image"
|
||||
/>
|
||||
)}
|
||||
<canvas
|
||||
ref={setDecoderCanvas}
|
||||
className={clsx(styles.videoElement, useDecoder ? styles.videoOpacityVisible : styles.videoOpacityHidden)}
|
||||
style={{position: 'absolute', inset: 0, pointerEvents: 'none'}}
|
||||
className={clsx(
|
||||
styles.videoElement,
|
||||
decoderCanvasVisible ? styles.videoOpacityVisible : styles.videoOpacityHidden,
|
||||
)}
|
||||
style={{position: 'absolute', inset: 0, pointerEvents: 'none', zIndex: decoderCanvasVisible ? 3 : 0}}
|
||||
tabIndex={-1}
|
||||
aria-hidden={useDecoder ? undefined : true}
|
||||
aria-hidden={decoderCanvasVisible ? undefined : true}
|
||||
width={renderedDimensions.width}
|
||||
height={renderedDimensions.height}
|
||||
data-flx="channel.embeds.media.embed-gifv.embed-gif.video-element--2"
|
||||
|
||||
Reference in new issue
Block a user