168 lines
5.8 KiB
TypeScript
168 lines
5.8 KiB
TypeScript
import React, { memo, useState } from '../../lib/teact/teact';
|
|
import { getGlobal } from '../../global';
|
|
|
|
import type { FC } from '../../lib/teact/teact';
|
|
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
|
import type { ApiSticker } from '../../api/types';
|
|
|
|
import { IS_WEBM_SUPPORTED } from '../../util/environment';
|
|
import * as mediaLoader from '../../util/mediaLoader';
|
|
import buildClassName from '../../util/buildClassName';
|
|
import generateIdFor from '../../util/generateIdFor';
|
|
import { getStickerPreviewHash } from '../../global/helpers';
|
|
import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors';
|
|
|
|
import useMedia from '../../hooks/useMedia';
|
|
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
|
import useThumbnail from '../../hooks/useThumbnail';
|
|
import useMediaTransition from '../../hooks/useMediaTransition';
|
|
import useFlag from '../../hooks/useFlag';
|
|
|
|
import AnimatedSticker from './AnimatedSticker';
|
|
import OptimizedVideo from '../ui/OptimizedVideo';
|
|
|
|
import styles from './StickerView.module.scss';
|
|
|
|
type OwnProps = {
|
|
containerRef: React.RefObject<HTMLDivElement>;
|
|
sticker: ApiSticker;
|
|
thumbClassName?: string;
|
|
fullMediaHash?: string;
|
|
fullMediaClassName?: string;
|
|
isSmall?: boolean;
|
|
size?: number;
|
|
customColor?: [number, number, number];
|
|
loopLimit?: number;
|
|
shouldLoop?: boolean;
|
|
shouldPreloadPreview?: boolean;
|
|
forceOnHeavyAnimation?: boolean;
|
|
observeIntersectionForLoading?: ObserveFn;
|
|
observeIntersectionForPlaying?: ObserveFn;
|
|
noLoad?: boolean;
|
|
noPlay?: boolean;
|
|
withSharedAnimation?: boolean;
|
|
cacheBuster?: number;
|
|
onVideoEnded?: AnyToVoidFunction;
|
|
onAnimatedStickerLoop?: AnyToVoidFunction;
|
|
};
|
|
|
|
const SHARED_PREFIX = 'shared';
|
|
const STICKER_SIZE = 24;
|
|
const ID_STORE = {};
|
|
|
|
const StickerView: FC<OwnProps> = ({
|
|
containerRef,
|
|
sticker,
|
|
thumbClassName,
|
|
fullMediaHash,
|
|
fullMediaClassName,
|
|
isSmall,
|
|
size = STICKER_SIZE,
|
|
customColor,
|
|
loopLimit,
|
|
shouldLoop = false,
|
|
shouldPreloadPreview,
|
|
forceOnHeavyAnimation,
|
|
observeIntersectionForLoading,
|
|
observeIntersectionForPlaying,
|
|
noLoad,
|
|
noPlay,
|
|
withSharedAnimation,
|
|
cacheBuster,
|
|
onVideoEnded,
|
|
onAnimatedStickerLoop,
|
|
}) => {
|
|
const {
|
|
id, isLottie, stickerSetInfo, emoji,
|
|
} = sticker;
|
|
const isUnsupportedVideo = sticker.isVideo && !IS_WEBM_SUPPORTED;
|
|
const isVideo = sticker.isVideo && !isUnsupportedVideo;
|
|
const isStatic = !isLottie && !isVideo;
|
|
const previewMediaHash = getStickerPreviewHash(sticker.id);
|
|
|
|
const isIntersectingForLoading = useIsIntersecting(containerRef, observeIntersectionForLoading);
|
|
const shouldLoad = isIntersectingForLoading && !noLoad;
|
|
const isIntersectingForPlaying = (
|
|
useIsIntersecting(containerRef, observeIntersectionForPlaying)
|
|
&& isIntersectingForLoading
|
|
);
|
|
const shouldPlay = isIntersectingForPlaying && !noPlay;
|
|
|
|
const thumbDataUri = useThumbnail(sticker);
|
|
// Use preview instead of thumb but only if it's already loaded
|
|
const [preloadedPreviewData] = useState(mediaLoader.getFromMemory(previewMediaHash));
|
|
const thumbData = preloadedPreviewData || thumbDataUri;
|
|
|
|
const shouldForcePreview = isUnsupportedVideo || (isStatic && isSmall);
|
|
fullMediaHash ||= shouldForcePreview ? previewMediaHash : `sticker${id}`;
|
|
|
|
// If preloaded preview is forced, it will render as thumb, so no need to load it again
|
|
const shouldSkipFullMedia = Boolean(fullMediaHash === previewMediaHash && preloadedPreviewData);
|
|
|
|
const fullMediaData = useMedia(fullMediaHash, !shouldLoad || shouldSkipFullMedia, undefined, cacheBuster);
|
|
// If Lottie data is loaded we will only render thumb if it's good enough (from preview)
|
|
const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData && !preloadedPreviewData));
|
|
const isFullMediaReady = fullMediaData && (isStatic || isPlayerReady);
|
|
|
|
const thumbClassNames = useMediaTransition(thumbData && !isFullMediaReady);
|
|
const fullMediaClassNames = useMediaTransition(isFullMediaReady);
|
|
const noTransition = isLottie && preloadedPreviewData;
|
|
|
|
// Preload preview for Message Input and local message
|
|
useMedia(previewMediaHash, !shouldLoad || !shouldPreloadPreview, undefined, cacheBuster);
|
|
|
|
const [randomIdPrefix] = useState(generateIdFor(ID_STORE, true));
|
|
const idKey = [
|
|
(withSharedAnimation ? SHARED_PREFIX : randomIdPrefix), id, size, customColor?.join(','),
|
|
].filter(Boolean).join('_');
|
|
|
|
return (
|
|
<>
|
|
<img
|
|
src={thumbData}
|
|
className={buildClassName(styles.thumb, noTransition && styles.noTransition, thumbClassName, thumbClassNames)}
|
|
alt=""
|
|
/>
|
|
{isLottie ? (
|
|
<AnimatedSticker
|
|
key={idKey}
|
|
id={idKey}
|
|
size={size}
|
|
className={buildClassName(
|
|
styles.media, noTransition && styles.noTransition, fullMediaClassName, fullMediaClassNames,
|
|
)}
|
|
tgsUrl={fullMediaData}
|
|
play={shouldPlay}
|
|
color={customColor}
|
|
noLoop={!shouldLoop}
|
|
forceOnHeavyAnimation={forceOnHeavyAnimation}
|
|
isLowPriority={isSmall && !selectIsAlwaysHighPriorityEmoji(getGlobal(), stickerSetInfo)}
|
|
onLoad={markPlayerReady}
|
|
onLoop={onAnimatedStickerLoop}
|
|
onEnded={onAnimatedStickerLoop}
|
|
/>
|
|
) : isVideo ? (
|
|
<OptimizedVideo
|
|
canPlay={shouldPlay && shouldLoop}
|
|
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
|
src={fullMediaData}
|
|
playsInline
|
|
muted
|
|
loop={!loopLimit}
|
|
disablePictureInPicture
|
|
onPlay={markPlayerReady}
|
|
onEnded={onVideoEnded}
|
|
/>
|
|
) : (
|
|
<img
|
|
className={buildClassName(styles.media, fullMediaClassName, fullMediaClassNames)}
|
|
src={fullMediaData}
|
|
alt={emoji}
|
|
/>
|
|
)}
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default memo(StickerView);
|