import React, { FC, useCallback, useRef, useState, } from '../../../lib/teact/teact'; import { ApiMessage } from '../../../api/types'; import { IMediaDimensions } from './helpers/calculateAlbumLayout'; import { formatMediaDuration } from '../../../util/dateFormat'; import buildClassName from '../../../util/buildClassName'; import { calculateVideoDimensions } from '../../common/helpers/mediaDimensions'; import { canMessagePlayVideoInline, getMediaTransferState, getMessageMediaFormat, getMessageMediaHash, isForwardedMessage, isOwnMessage, } from '../../../modules/helpers'; import { ObserveFn, useIsIntersecting } from '../../../hooks/useIntersectionObserver'; import useMediaWithDownloadProgress from '../../../hooks/useMediaWithDownloadProgress'; import useMedia from '../../../hooks/useMedia'; import useShowTransition from '../../../hooks/useShowTransition'; import useTransitionForMedia from '../../../hooks/useTransitionForMedia'; import usePrevious from '../../../hooks/usePrevious'; import useBuffering from '../../../hooks/useBuffering'; import useHeavyAnimationCheckForVideo from '../../../hooks/useHeavyAnimationCheckForVideo'; import useVideoCleanup from '../../../hooks/useVideoCleanup'; import usePauseOnInactive from './hooks/usePauseOnInactive'; import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef'; import ProgressSpinner from '../../ui/ProgressSpinner'; export type OwnProps = { id?: string; message: ApiMessage; observeIntersection: ObserveFn; shouldAutoLoad?: boolean; shouldAutoPlay?: boolean; uploadProgress?: number; dimensions?: IMediaDimensions; lastSyncTime?: number; onClick?: (id: number) => void; onCancelUpload?: (message: ApiMessage) => void; }; const Video: FC = ({ id, message, observeIntersection, shouldAutoLoad, shouldAutoPlay, uploadProgress, lastSyncTime, dimensions, onClick, onCancelUpload, }) => { // eslint-disable-next-line no-null/no-null const ref = useRef(null); // eslint-disable-next-line no-null/no-null const videoRef = useRef(null); const video = message.content.video!; const localBlobUrl = video.blobUrl; const canPlayInline = Boolean(localBlobUrl) || canMessagePlayVideoInline(video); const isIntersecting = useIsIntersecting(ref, observeIntersection); const [isDownloadAllowed, setIsDownloadAllowed] = useState(shouldAutoLoad); const shouldDownload = Boolean(isDownloadAllowed && isIntersecting && lastSyncTime); const [isPlayAllowed, setIsPlayAllowed] = useState(shouldAutoPlay); const previewBlobUrl = useMedia( getMessageMediaHash(message, 'pictogram'), !(isIntersecting && lastSyncTime), getMessageMediaFormat(message, 'pictogram'), lastSyncTime, ); const thumbRef = useBlurredMediaThumbRef(message); const { mediaData, downloadProgress } = useMediaWithDownloadProgress( getMessageMediaHash(message, 'inline'), !shouldDownload, getMessageMediaFormat(message, 'inline'), lastSyncTime, ); const fullMediaData = localBlobUrl || mediaData; const isInline = Boolean(canPlayInline && isIntersecting && fullMediaData); const { isBuffered, bufferingHandlers } = useBuffering(!shouldAutoLoad); const { isUploading, isTransferring, transferProgress } = getMediaTransferState( message, uploadProgress || downloadProgress, shouldDownload && (canPlayInline && !isBuffered), ); const wasDownloadDisabled = usePrevious(isDownloadAllowed) === false; const { shouldRender: shouldRenderSpinner, transitionClassNames: spinnerClassNames, } = useShowTransition(isTransferring, undefined, wasDownloadDisabled); const { shouldRenderThumb, transitionClassNames } = useTransitionForMedia(fullMediaData, 'slow'); const [playProgress, setPlayProgress] = useState(0); const handleTimeUpdate = useCallback((e: React.SyntheticEvent) => { setPlayProgress(Math.max(0, e.currentTarget.currentTime - 1)); }, []); const isOwn = isOwnMessage(message); const isForwarded = isForwardedMessage(message); const { width, height } = dimensions || calculateVideoDimensions(video, isOwn, isForwarded); useHeavyAnimationCheckForVideo(videoRef, Boolean(isInline && shouldAutoPlay)); usePauseOnInactive(videoRef, isPlayAllowed); useVideoCleanup(videoRef, [isInline]); const handleClick = useCallback(() => { if (isUploading) { if (onCancelUpload) { onCancelUpload(message); } } else if (canPlayInline && !fullMediaData) { setIsDownloadAllowed((isAllowed) => !isAllowed); } else if (canPlayInline && fullMediaData && !isPlayAllowed) { setIsPlayAllowed(true); videoRef.current!.play(); } else if (onClick) { onClick(message.id); } }, [isUploading, canPlayInline, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]); const className = buildClassName('media-inner dark', !isUploading && 'interactive'); const videoClassName = buildClassName('full-media', transitionClassNames); const videoStyle = previewBlobUrl ? `background-image: url(${previewBlobUrl}); background-size: cover` : 'background: transparent'; const style = dimensions ? `width: ${width}px; height: ${height}px; left: ${dimensions.x}px; top: ${dimensions.y}px;` : ''; const shouldRenderInlineVideo = isInline; const shouldRenderHqPreview = !canPlayInline && mediaData; const shouldRenderPlayButton = !canPlayInline || (isDownloadAllowed && !isPlayAllowed && !shouldRenderSpinner); const shouldRenderDownloadButton = canPlayInline && !isDownloadAllowed; return (
{((!isInline || shouldRenderThumb) && !previewBlobUrl) && ( )} {previewBlobUrl && fullMediaData && ( )} {shouldRenderInlineVideo && ( )} {shouldRenderHqPreview && ( )} {shouldRenderPlayButton && ( )} {shouldRenderSpinner && (
)} {shouldRenderDownloadButton && ( )} {isTransferring && !canPlayInline ? ( {Math.round(transferProgress * 100)}% ) : isTransferring && canPlayInline ? ( ... ) : (
{video.isGif ? 'GIF' : formatMediaDuration(video.duration - playProgress)}
)}
); }; export default Video;