229 lines
7.8 KiB
TypeScript
229 lines
7.8 KiB
TypeScript
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<OwnProps> = ({
|
|
id,
|
|
message,
|
|
observeIntersection,
|
|
shouldAutoLoad,
|
|
shouldAutoPlay,
|
|
uploadProgress,
|
|
lastSyncTime,
|
|
dimensions,
|
|
onClick,
|
|
onCancelUpload,
|
|
}) => {
|
|
// eslint-disable-next-line no-null/no-null
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
// eslint-disable-next-line no-null/no-null
|
|
const videoRef = useRef<HTMLVideoElement>(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<number>(0);
|
|
const handleTimeUpdate = useCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
|
|
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 (
|
|
<div
|
|
ref={ref}
|
|
id={id}
|
|
className={className}
|
|
// @ts-ignore teact feature
|
|
style={style}
|
|
onClick={isUploading ? undefined : handleClick}
|
|
>
|
|
{((!isInline || shouldRenderThumb) && !previewBlobUrl)
|
|
&& (
|
|
<canvas
|
|
ref={thumbRef}
|
|
className="thumbnail"
|
|
// @ts-ignore teact feature
|
|
style={`width: ${width}px; height: ${height}px;`}
|
|
/>
|
|
)}
|
|
{previewBlobUrl && fullMediaData && (
|
|
<img
|
|
src={previewBlobUrl}
|
|
className="thumbnail"
|
|
// @ts-ignore teact feature
|
|
style={`width: ${width}px; height: ${height}px;`}
|
|
alt=""
|
|
/>
|
|
)}
|
|
|
|
{shouldRenderInlineVideo && (
|
|
<video
|
|
ref={videoRef}
|
|
className={videoClassName}
|
|
width={width}
|
|
height={height}
|
|
autoPlay={isPlayAllowed}
|
|
muted
|
|
loop
|
|
playsInline
|
|
// @ts-ignore teact feature
|
|
style={videoStyle}
|
|
// eslint-disable-next-line react/jsx-props-no-spreading
|
|
{...bufferingHandlers}
|
|
onTimeUpdate={handleTimeUpdate}
|
|
>
|
|
<source src={fullMediaData} />
|
|
</video>
|
|
)}
|
|
{shouldRenderHqPreview && (
|
|
<img
|
|
src={mediaData}
|
|
className={`full-media ${transitionClassNames}`}
|
|
width={width}
|
|
height={height}
|
|
alt=""
|
|
/>
|
|
)}
|
|
{shouldRenderPlayButton && (
|
|
<i className="icon-large-play" />
|
|
)}
|
|
{shouldRenderSpinner && (
|
|
<div className={`media-loading ${spinnerClassNames}`}>
|
|
<ProgressSpinner progress={transferProgress} onClick={isUploading ? handleClick : undefined} />
|
|
</div>
|
|
)}
|
|
{shouldRenderDownloadButton && (
|
|
<i className="icon-download" />
|
|
)}
|
|
{isTransferring && !canPlayInline ? (
|
|
<span className="message-upload-progress">{Math.round(transferProgress * 100)}%</span>
|
|
) : isTransferring && canPlayInline ? (
|
|
<span className="message-upload-progress">...</span>
|
|
) : (
|
|
<div className="message-media-duration">
|
|
{video.isGif ? 'GIF' : formatMediaDuration(video.duration - playProgress)}
|
|
</div>
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default Video;
|