Message / Video: Better progressive loading
This commit is contained in:
parent
128d25047f
commit
a050b10aa8
@ -110,7 +110,7 @@ const ProfilePhoto: FC<OwnProps> = ({
|
||||
disablePictureInPicture
|
||||
loop
|
||||
playsInline
|
||||
onPlay={markVideoReady}
|
||||
onReady={markVideoReady}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
|
||||
@ -170,7 +170,7 @@ const StickerView: FC<OwnProps> = ({
|
||||
muted
|
||||
loop={!loopLimit}
|
||||
disablePictureInPicture
|
||||
onPlay={markPlayerReady}
|
||||
onReady={markPlayerReady}
|
||||
onEnded={onVideoEnded}
|
||||
/>
|
||||
) : (
|
||||
|
||||
@ -338,7 +338,7 @@ function getNodes(origin: MediaViewerOrigin, message?: ApiMessage) {
|
||||
|
||||
return {
|
||||
container,
|
||||
mediaEl: mediaEls?.[mediaEls.length - 1],
|
||||
mediaEl: mediaEls?.[0],
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@ -73,7 +73,7 @@ export const useMediaProps = ({
|
||||
return getChatAvatarHash(avatarOwner, isFull ? 'big' : 'normal');
|
||||
}
|
||||
}
|
||||
return message && getMessageMediaHash(message, isFull ? 'viewerFull' : 'viewerPreview');
|
||||
return message && getMessageMediaHash(message, isFull ? 'full' : 'preview');
|
||||
}, [avatarOwner, message, avatarMedia, mediaId]);
|
||||
|
||||
const pictogramBlobUrl = useMedia(
|
||||
@ -97,7 +97,7 @@ export const useMediaProps = ({
|
||||
} = useMediaWithLoadProgress(
|
||||
getMediaHash(true),
|
||||
undefined,
|
||||
message && getMessageMediaFormat(message, 'viewerFull'),
|
||||
message && getMessageMediaFormat(message, 'full'),
|
||||
undefined,
|
||||
delay,
|
||||
);
|
||||
|
||||
@ -81,9 +81,9 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
const { openHistoryCalendar } = getActions();
|
||||
|
||||
const {
|
||||
observeIntersectionForMedia,
|
||||
observeIntersectionForReading,
|
||||
observeIntersectionForAnimatedStickers,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef);
|
||||
|
||||
const {
|
||||
@ -144,8 +144,8 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
key={message.id}
|
||||
message={message}
|
||||
observeIntersectionForReading={observeIntersectionForReading}
|
||||
observeIntersectionForLoading={observeIntersectionForMedia}
|
||||
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
memoFirstUnreadIdRef={memoFirstUnreadIdRef}
|
||||
appearanceOrder={messageCountToAnimate - ++appearanceIndex}
|
||||
isLastInList={isLastInList}
|
||||
@ -196,8 +196,8 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
key={key}
|
||||
message={message}
|
||||
observeIntersectionForBottom={observeIntersectionForReading}
|
||||
observeIntersectionForMedia={observeIntersectionForMedia}
|
||||
observeIntersectionForAnimatedStickers={observeIntersectionForAnimatedStickers}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
album={album}
|
||||
noAvatars={noAvatars}
|
||||
withAvatar={position.isLastInGroup && withUsers && !isOwn && !(message.id === threadTopMessageId)}
|
||||
|
||||
@ -7,9 +7,9 @@ import { IS_ANDROID, IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useBackgroundMode from '../../../hooks/useBackgroundMode';
|
||||
|
||||
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
|
||||
const INTERSECTION_MARGIN_FOR_MEDIA = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
|
||||
const INTERSECTION_THROTTLE_FOR_READING = 150;
|
||||
const INTERSECTION_THROTTLE_FOR_MEDIA = IS_ANDROID ? 1000 : 350;
|
||||
const INTERSECTION_MARGIN_FOR_LOADING = IS_SINGLE_COLUMN_LAYOUT ? 300 : 500;
|
||||
|
||||
export default function useMessageObservers(
|
||||
type: MessageListType,
|
||||
@ -18,14 +18,6 @@ export default function useMessageObservers(
|
||||
) {
|
||||
const { markMessageListRead, markMentionsRead, animateUnreadReaction } = getActions();
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForMedia,
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
|
||||
margin: INTERSECTION_MARGIN_FOR_MEDIA,
|
||||
});
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForReading, freeze: freezeForReading, unfreeze: unfreezeForReading,
|
||||
} = useIntersectionObserver({
|
||||
@ -78,14 +70,22 @@ export default function useMessageObservers(
|
||||
|
||||
useBackgroundMode(freezeForReading, unfreezeForReading);
|
||||
|
||||
const { observe: observeIntersectionForAnimatedStickers } = useIntersectionObserver({
|
||||
const {
|
||||
observe: observeIntersectionForLoading,
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
|
||||
margin: INTERSECTION_MARGIN_FOR_LOADING,
|
||||
});
|
||||
|
||||
const { observe: observeIntersectionForPlaying } = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: INTERSECTION_THROTTLE_FOR_MEDIA,
|
||||
});
|
||||
|
||||
return {
|
||||
observeIntersectionForMedia,
|
||||
observeIntersectionForReading,
|
||||
observeIntersectionForAnimatedStickers,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
};
|
||||
}
|
||||
|
||||
@ -84,7 +84,7 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
<PhotoWithSelect
|
||||
id={`album-media-${getMessageHtmlId(message.id)}`}
|
||||
message={message}
|
||||
observeIntersection={observeIntersection}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
canAutoLoad={canAutoLoad}
|
||||
shouldAffectAppendix={shouldAffectAppendix}
|
||||
uploadProgress={uploadProgress}
|
||||
@ -101,7 +101,7 @@ const Album: FC<OwnProps & StateProps> = ({
|
||||
<VideoWithSelect
|
||||
id={`album-media-${getMessageHtmlId(message.id)}`}
|
||||
message={message}
|
||||
observeIntersection={observeIntersection}
|
||||
observeIntersectionForLoading={observeIntersection}
|
||||
canAutoLoad={canAutoLoad}
|
||||
canAutoPlay={canAutoPlay}
|
||||
uploadProgress={uploadProgress}
|
||||
|
||||
@ -143,8 +143,8 @@ type OwnProps =
|
||||
{
|
||||
message: ApiMessage;
|
||||
observeIntersectionForBottom: ObserveFn;
|
||||
observeIntersectionForMedia: ObserveFn;
|
||||
observeIntersectionForAnimatedStickers: ObserveFn;
|
||||
observeIntersectionForLoading: ObserveFn;
|
||||
observeIntersectionForPlaying: ObserveFn;
|
||||
album?: IAlbum;
|
||||
noAvatars?: boolean;
|
||||
withAvatar?: boolean;
|
||||
@ -235,8 +235,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
message,
|
||||
chatUsername,
|
||||
observeIntersectionForBottom,
|
||||
observeIntersectionForMedia,
|
||||
observeIntersectionForAnimatedStickers,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
album,
|
||||
noAvatars,
|
||||
withAvatar,
|
||||
@ -634,7 +634,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
text={hiddenName}
|
||||
lastSyncTime={lastSyncTime}
|
||||
onClick={(avatarUser || avatarChat) ? handleAvatarClick : undefined}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
animationLevel={animationLevel}
|
||||
withVideo
|
||||
/>
|
||||
@ -693,16 +693,16 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
noUserColors={isOwn}
|
||||
isProtected={isProtected}
|
||||
sender={replyMessageSender}
|
||||
observeIntersectionForLoading={observeIntersectionForMedia}
|
||||
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
onClick={handleReplyClick}
|
||||
/>
|
||||
)}
|
||||
{sticker && (
|
||||
<Sticker
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
shouldLoop={shouldLoopStickers}
|
||||
lastSyncTime={lastSyncTime}
|
||||
shouldPlayEffect={(
|
||||
@ -719,7 +719,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
customEmojiId={animatedCustomEmoji}
|
||||
withEffects={isUserId(chatId)}
|
||||
isOwn={isOwn}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
lastSyncTime={lastSyncTime}
|
||||
forceLoadPreview={isLocal}
|
||||
messageId={messageId}
|
||||
@ -732,7 +732,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
emoji={animatedEmoji}
|
||||
withEffects={isUserId(chatId)}
|
||||
isOwn={isOwn}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
lastSyncTime={lastSyncTime}
|
||||
forceLoadPreview={isLocal}
|
||||
messageId={messageId}
|
||||
@ -744,7 +744,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
<Album
|
||||
album={album!}
|
||||
albumLayout={albumLayout!}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
isOwn={isOwn}
|
||||
isProtected={isProtected}
|
||||
hasCustomAppendix={hasCustomAppendix}
|
||||
@ -762,7 +762,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{!isAlbum && photo && (
|
||||
<Photo
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
uploadProgress={uploadProgress}
|
||||
@ -777,7 +777,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{!isAlbum && video && video.isRound && (
|
||||
<RoundVideo
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
lastSyncTime={lastSyncTime}
|
||||
isDownloading={isDownloading}
|
||||
@ -786,7 +786,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{!isAlbum && video && !video.isRound && (
|
||||
<Video
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
canAutoPlay={canAutoPlayMedia}
|
||||
@ -824,7 +825,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{document && (
|
||||
<Document
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
autoLoadFileMaxSizeMb={autoLoadFileMaxSizeMb}
|
||||
uploadProgress={uploadProgress}
|
||||
@ -876,8 +877,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
emojiSize={emojiSize}
|
||||
highlight={highlight}
|
||||
isProtected={isProtected}
|
||||
observeIntersectionForLoading={observeIntersectionForMedia}
|
||||
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
withTranslucentThumbs={isCustomShape}
|
||||
/>
|
||||
{metaPosition === 'in-text' && renderReactionsAndMeta()}
|
||||
@ -887,7 +888,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
{webPage && (
|
||||
<WebPage
|
||||
message={message}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
observeIntersection={observeIntersectionForLoading}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoadMedia}
|
||||
canAutoPlay={canAutoPlayMedia}
|
||||
@ -960,8 +961,8 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
<CustomEmoji
|
||||
documentId={senderEmojiStatus.documentId}
|
||||
loopLimit={EMOJI_STATUS_LOOP_LIMIT}
|
||||
observeIntersectionForLoading={observeIntersectionForMedia}
|
||||
observeIntersectionForPlaying={observeIntersectionForAnimatedStickers}
|
||||
observeIntersectionForLoading={observeIntersectionForLoading}
|
||||
observeIntersectionForPlaying={observeIntersectionForPlaying}
|
||||
/>
|
||||
)}
|
||||
{!asForwarded && !senderEmojiStatus && senderIsPremium && <PremiumIcon />}
|
||||
|
||||
@ -88,7 +88,9 @@ const Photo: FC<OwnProps> = ({
|
||||
const fullMediaData = localBlobUrl || mediaData;
|
||||
|
||||
const [withThumb] = useState(!fullMediaData);
|
||||
const thumbRef = useBlurredMediaThumbRef(message, fullMediaData);
|
||||
const noThumb = Boolean(fullMediaData);
|
||||
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
|
||||
const thumbClassNames = useMediaTransition(!noThumb);
|
||||
|
||||
const {
|
||||
loadProgress: downloadProgress,
|
||||
@ -105,7 +107,6 @@ const Photo: FC<OwnProps> = ({
|
||||
);
|
||||
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||
|
||||
const transitionClassNames = useMediaTransition(fullMediaData);
|
||||
const {
|
||||
shouldRender: shouldRenderSpinner,
|
||||
transitionClassNames: spinnerClassNames,
|
||||
@ -169,21 +170,21 @@ const Photo: FC<OwnProps> = ({
|
||||
style={style}
|
||||
onClick={isUploading ? undefined : handleClick}
|
||||
>
|
||||
{withThumb && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className="thumbnail"
|
||||
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={fullMediaData}
|
||||
className={`full-media ${transitionClassNames}`}
|
||||
className="full-media"
|
||||
width={width}
|
||||
height={height}
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
/>
|
||||
{withThumb && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
|
||||
/>
|
||||
)}
|
||||
{isProtected && <span className="protector" />}
|
||||
{shouldRenderSpinner && !shouldRenderDownloadButton && (
|
||||
<div className={`media-loading ${spinnerClassNames}`}>
|
||||
|
||||
@ -4,13 +4,6 @@
|
||||
height: 15rem;
|
||||
cursor: pointer;
|
||||
|
||||
.thumbnail-wrapper {
|
||||
width: 15rem;
|
||||
height: 15rem;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.video-wrapper {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
@ -19,6 +12,10 @@
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
canvas {
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.progress {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
|
||||
@ -82,7 +82,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
const isTransferring = (isLoadAllowed && !isBuffered) || isDownloading;
|
||||
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||
|
||||
const transitionClassNames = useMediaTransition(mediaData);
|
||||
const thumbClassNames = useMediaTransition(!mediaData);
|
||||
const {
|
||||
shouldRender: shouldSpinnerRender,
|
||||
transitionClassNames: spinnerClassNames,
|
||||
@ -180,30 +180,19 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
setProgress(playerEl.currentTime / playerEl.duration);
|
||||
}, []);
|
||||
|
||||
const videoClassName = buildClassName('full-media', transitionClassNames);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className="RoundVideo media-inner"
|
||||
onClick={handleClick}
|
||||
>
|
||||
{withThumb && (
|
||||
<div className="thumbnail-wrapper">
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className="thumbnail"
|
||||
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{mediaData && (
|
||||
<div className="video-wrapper">
|
||||
<OptimizedVideo
|
||||
canPlay={shouldPlay}
|
||||
ref={playerRef}
|
||||
src={mediaData}
|
||||
className={videoClassName}
|
||||
className="full-media"
|
||||
width={ROUND_VIDEO_DIMENSIONS_PX}
|
||||
height={ROUND_VIDEO_DIMENSIONS_PX}
|
||||
autoPlay
|
||||
@ -218,6 +207,13 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{withThumb && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
style={`width: ${ROUND_VIDEO_DIMENSIONS_PX}px; height: ${ROUND_VIDEO_DIMENSIONS_PX}px`}
|
||||
/>
|
||||
)}
|
||||
<div className="progress" ref={playingProgressRef} />
|
||||
{shouldSpinnerRender && (
|
||||
<div className={`media-loading ${spinnerClassNames}`}>
|
||||
|
||||
@ -11,21 +11,22 @@ import { calculateVideoDimensions } from '../../common/helpers/mediaDimensions';
|
||||
import {
|
||||
getMediaTransferState,
|
||||
getMessageMediaFormat,
|
||||
getMessageMediaHash,
|
||||
getMessageMediaHash, getMessageMediaThumbDataUri,
|
||||
getMessageVideo,
|
||||
getMessageWebPageVideo,
|
||||
isForwardedMessage,
|
||||
isOwnMessage,
|
||||
} from '../../../global/helpers';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
|
||||
import useMedia from '../../../hooks/useMedia';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import usePrevious from '../../../hooks/usePrevious';
|
||||
import useBuffering from '../../../hooks/useBuffering';
|
||||
import useMediaTransition from '../../../hooks/useMediaTransition';
|
||||
import useBlurredMediaThumbRef from './hooks/useBlurredMediaThumbRef';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
import ProgressSpinner from '../../ui/ProgressSpinner';
|
||||
import OptimizedVideo from '../../ui/OptimizedVideo';
|
||||
@ -33,7 +34,8 @@ import OptimizedVideo from '../../ui/OptimizedVideo';
|
||||
export type OwnProps = {
|
||||
id?: string;
|
||||
message: ApiMessage;
|
||||
observeIntersection: ObserveFn;
|
||||
observeIntersectionForLoading: ObserveFn;
|
||||
observeIntersectionForPlaying?: ObserveFn;
|
||||
noAvatars?: boolean;
|
||||
canAutoLoad?: boolean;
|
||||
canAutoPlay?: boolean;
|
||||
@ -50,7 +52,8 @@ export type OwnProps = {
|
||||
const Video: FC<OwnProps> = ({
|
||||
id,
|
||||
message,
|
||||
observeIntersection,
|
||||
observeIntersectionForLoading,
|
||||
observeIntersectionForPlaying,
|
||||
noAvatars,
|
||||
canAutoLoad,
|
||||
canAutoPlay,
|
||||
@ -71,32 +74,41 @@ const Video: FC<OwnProps> = ({
|
||||
const video = (getMessageVideo(message) || getMessageWebPageVideo(message))!;
|
||||
const localBlobUrl = video.blobUrl;
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
const isIntersectingForLoading = useIsIntersecting(ref, observeIntersectionForLoading);
|
||||
const isIntersectingForPlaying = (
|
||||
useIsIntersecting(ref, observeIntersectionForPlaying)
|
||||
&& isIntersectingForLoading
|
||||
);
|
||||
const wasIntersectedRef = useRef(isIntersectingForLoading);
|
||||
if (isIntersectingForPlaying && !wasIntersectedRef.current) {
|
||||
wasIntersectedRef.current = true;
|
||||
}
|
||||
|
||||
const [isLoadAllowed, setIsLoadAllowed] = useState(canAutoLoad);
|
||||
const shouldLoad = Boolean(isLoadAllowed && isIntersecting && lastSyncTime);
|
||||
const shouldLoad = Boolean(isLoadAllowed && isIntersectingForLoading && lastSyncTime);
|
||||
const [isPlayAllowed, setIsPlayAllowed] = useState(canAutoPlay);
|
||||
|
||||
const previewBlobUrl = useMedia(
|
||||
getMessageMediaHash(message, 'pictogram'),
|
||||
!(isIntersecting && lastSyncTime),
|
||||
getMessageMediaFormat(message, 'pictogram'),
|
||||
lastSyncTime,
|
||||
);
|
||||
const previewClassNames = useMediaTransition(previewBlobUrl);
|
||||
|
||||
const fullMediaHash = getMessageMediaHash(message, 'inline');
|
||||
const [isFullMediaPreloaded] = useState(Boolean(fullMediaHash && mediaLoader.getFromMemory(fullMediaHash)));
|
||||
const { mediaData, loadProgress } = useMediaWithLoadProgress(
|
||||
getMessageMediaHash(message, 'inline'),
|
||||
!shouldLoad,
|
||||
getMessageMediaFormat(message, 'inline'),
|
||||
lastSyncTime,
|
||||
fullMediaHash, !shouldLoad, getMessageMediaFormat(message, 'inline'), lastSyncTime,
|
||||
);
|
||||
const fullMediaData = localBlobUrl || mediaData;
|
||||
const isInline = Boolean(isIntersecting && fullMediaData);
|
||||
const [isPlayerReady, markPlayerReady] = useFlag();
|
||||
|
||||
// Thumbnail is always rendered, so we can only disable blur if we have a preview
|
||||
const [withThumb] = useState(!previewBlobUrl);
|
||||
const thumbRef = useBlurredMediaThumbRef(message, previewBlobUrl);
|
||||
const hasThumb = Boolean(getMessageMediaThumbDataUri(message));
|
||||
|
||||
const previewMediaHash = getMessageMediaHash(message, 'preview');
|
||||
const [isPreviewPreloaded] = useState(Boolean(previewMediaHash && mediaLoader.getFromMemory(previewMediaHash)));
|
||||
const canLoadPreview = isIntersectingForLoading && lastSyncTime;
|
||||
const previewBlobUrl = useMedia(previewMediaHash, !canLoadPreview, undefined, lastSyncTime);
|
||||
const previewClassNames = useMediaTransition((hasThumb || previewBlobUrl) && !isPlayerReady);
|
||||
|
||||
const noThumb = !hasThumb || previewBlobUrl || isPlayerReady;
|
||||
const thumbRef = useBlurredMediaThumbRef(message, noThumb);
|
||||
const thumbClassNames = useMediaTransition(!noThumb);
|
||||
|
||||
const isInline = fullMediaData && wasIntersectedRef.current;
|
||||
|
||||
const { loadProgress: downloadProgress } = useMediaWithLoadProgress(
|
||||
getMessageMediaHash(message, 'download'),
|
||||
@ -105,21 +117,20 @@ const Video: FC<OwnProps> = ({
|
||||
lastSyncTime,
|
||||
);
|
||||
|
||||
const { isBuffered, bufferingHandlers } = useBuffering(!canAutoLoad);
|
||||
const { isUploading, isTransferring, transferProgress } = getMediaTransferState(
|
||||
message,
|
||||
uploadProgress || (isDownloading ? downloadProgress : loadProgress),
|
||||
(shouldLoad && !isBuffered) || isDownloading,
|
||||
(shouldLoad && !isPlayerReady && !isFullMediaPreloaded) || isDownloading,
|
||||
);
|
||||
|
||||
const wasLoadDisabled = usePrevious(isLoadAllowed) === false;
|
||||
const {
|
||||
shouldRender: shouldRenderSpinner,
|
||||
transitionClassNames: spinnerClassNames,
|
||||
} = useShowTransition(isTransferring, undefined, wasLoadDisabled);
|
||||
const {
|
||||
shouldRender: shouldRenderPlayButton,
|
||||
transitionClassNames: playButtonClassNames,
|
||||
} = useShowTransition(isLoadAllowed && !isPlayAllowed && !shouldRenderSpinner);
|
||||
} = useShowTransition(Boolean((isLoadAllowed || fullMediaData) && !isPlayAllowed && !shouldRenderSpinner));
|
||||
|
||||
const [playProgress, setPlayProgress] = useState<number>(0);
|
||||
const handleTimeUpdate = useCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||
@ -149,10 +160,11 @@ const Video: FC<OwnProps> = ({
|
||||
}, [isUploading, isDownloading, fullMediaData, isPlayAllowed, onClick, onCancelUpload, message]);
|
||||
|
||||
const className = buildClassName('media-inner dark', !isUploading && 'interactive');
|
||||
const aspectRatio = withAspectRatio ? `aspect-ratio: ${(width / height).toFixed(3)}/ 1` : '';
|
||||
const style = dimensions
|
||||
? `width: ${width}px; height: ${height}px; left: ${dimensions.x}px; top: ${dimensions.y}px;${aspectRatio}`
|
||||
: '';
|
||||
|
||||
const dimensionsStyle = dimensions ? ` left: ${dimensions.x}px; top: ${dimensions.y}px;` : '';
|
||||
const aspectRatioStyle = withAspectRatio ? ` aspect-ratio: ${(width / height).toFixed(3)}/ 1;` : '';
|
||||
const style = `width: ${width}px; height: ${height}px;${dimensionsStyle}${aspectRatioStyle}`;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -161,47 +173,42 @@ const Video: FC<OwnProps> = ({
|
||||
style={style}
|
||||
onClick={isUploading ? undefined : handleClick}
|
||||
>
|
||||
{withThumb ? (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className="thumbnail"
|
||||
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={previewBlobUrl}
|
||||
className={buildClassName('thumbnail', previewClassNames)}
|
||||
style={`width: ${width}px; height: ${height}px;${aspectRatio}`}
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
/>
|
||||
)}
|
||||
{isInline && (
|
||||
<OptimizedVideo
|
||||
ref={videoRef}
|
||||
canPlay={isPlayAllowed}
|
||||
src={fullMediaData}
|
||||
className="full-media"
|
||||
canPlay={isPlayAllowed && isIntersectingForPlaying}
|
||||
width={width}
|
||||
height={height}
|
||||
muted
|
||||
loop
|
||||
playsInline
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
{...bufferingHandlers}
|
||||
draggable={!isProtected}
|
||||
onTimeUpdate={handleTimeUpdate}
|
||||
style={aspectRatio}
|
||||
onReady={markPlayerReady}
|
||||
/>
|
||||
)}
|
||||
<img
|
||||
src={previewBlobUrl}
|
||||
className={buildClassName('thumbnail', previewClassNames)}
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
/>
|
||||
{hasThumb && !isPreviewPreloaded && (
|
||||
<canvas
|
||||
ref={thumbRef}
|
||||
className={buildClassName('thumbnail', thumbClassNames)}
|
||||
/>
|
||||
)}
|
||||
{isProtected && <span className="protector" />}
|
||||
{shouldRenderPlayButton && <i className={buildClassName('icon-large-play', playButtonClassNames)} />}
|
||||
<i className={buildClassName('icon-large-play', playButtonClassNames)} />
|
||||
{shouldRenderSpinner && (
|
||||
<div className={buildClassName('media-loading', spinnerClassNames)}>
|
||||
<ProgressSpinner progress={transferProgress} onClick={handleClick} />
|
||||
</div>
|
||||
)}
|
||||
{!isLoadAllowed && (
|
||||
{!isLoadAllowed && !fullMediaData && (
|
||||
<i className="icon-download" />
|
||||
)}
|
||||
{isTransferring ? (
|
||||
|
||||
@ -121,7 +121,7 @@ const WebPage: FC<OwnProps> = ({
|
||||
{!inPreview && video && (
|
||||
<Video
|
||||
message={message}
|
||||
observeIntersection={observeIntersection!}
|
||||
observeIntersectionForLoading={observeIntersection!}
|
||||
noAvatars={noAvatars}
|
||||
canAutoLoad={canAutoLoad}
|
||||
canAutoPlay={canAutoPlay}
|
||||
|
||||
@ -500,7 +500,7 @@
|
||||
&.interactive {
|
||||
cursor: pointer;
|
||||
|
||||
&.dark video {
|
||||
&.dark video, &.dark canvas {
|
||||
background-color: #232323;
|
||||
}
|
||||
}
|
||||
|
||||
@ -4,10 +4,10 @@ import { IS_CANVAS_FILTER_SUPPORTED, IS_SINGLE_COLUMN_LAYOUT } from '../../../..
|
||||
import { getMessageMediaThumbDataUri } from '../../../../global/helpers';
|
||||
import useCanvasBlur from '../../../../hooks/useCanvasBlur';
|
||||
|
||||
export default function useBlurredMediaThumbRef(message: ApiMessage, fullMediaData?: string) {
|
||||
export default function useBlurredMediaThumbRef(message: ApiMessage, isDisabled?: boolean | string) {
|
||||
return useCanvasBlur(
|
||||
getMessageMediaThumbDataUri(message),
|
||||
Boolean(fullMediaData),
|
||||
Boolean(isDisabled),
|
||||
IS_SINGLE_COLUMN_LAYOUT && !IS_CANVAS_FILTER_SUPPORTED,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,33 +1,62 @@
|
||||
import React, { memo, useRef } from '../../lib/teact/teact';
|
||||
import React, { memo, useCallback, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import useVideoAutoPause from '../middle/message/hooks/useVideoAutoPause';
|
||||
import useVideoCleanup from '../../hooks/useVideoCleanup';
|
||||
import useBuffering from '../../hooks/useBuffering';
|
||||
import useOnChange from '../../hooks/useOnChange';
|
||||
|
||||
type OwnProps =
|
||||
{
|
||||
canPlay: boolean;
|
||||
ref?: React.RefObject<HTMLVideoElement>;
|
||||
canPlay: boolean;
|
||||
onReady?: NoneToVoidFunction;
|
||||
}
|
||||
& React.DetailedHTMLProps<React.VideoHTMLAttributes<HTMLVideoElement>, HTMLVideoElement>;
|
||||
|
||||
function OptimizedVideo({
|
||||
ref,
|
||||
canPlay,
|
||||
onReady,
|
||||
onTimeUpdate,
|
||||
...restProps
|
||||
}: OwnProps) {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const localRef = useRef<HTMLVideoElement>(null);
|
||||
|
||||
if (!ref) {
|
||||
ref = localRef;
|
||||
}
|
||||
|
||||
const { handlePlaying } = useVideoAutoPause(ref, canPlay);
|
||||
const { handlePlaying: handlePlayingForAutoPause } = useVideoAutoPause(ref, canPlay);
|
||||
useVideoCleanup(ref, []);
|
||||
|
||||
const isReadyRef = useRef(false);
|
||||
const handleReady = useCallback(() => {
|
||||
if (!isReadyRef.current) {
|
||||
onReady?.();
|
||||
isReadyRef.current = true;
|
||||
}
|
||||
}, [onReady]);
|
||||
|
||||
// This is only needed for browsers not allowing autoplay
|
||||
const { isBuffered, bufferingHandlers } = useBuffering(true, onTimeUpdate);
|
||||
const { onPlaying: handlePlayingForBuffering, ...otherBufferingHandlers } = bufferingHandlers;
|
||||
useOnChange(([prevIsBuffered]) => {
|
||||
if (prevIsBuffered === undefined) {
|
||||
return;
|
||||
}
|
||||
|
||||
handleReady();
|
||||
}, [isBuffered]);
|
||||
|
||||
const handlePlaying = useCallback((e) => {
|
||||
handlePlayingForAutoPause();
|
||||
handlePlayingForBuffering(e);
|
||||
handleReady();
|
||||
}, [handlePlayingForAutoPause, handlePlayingForBuffering, handleReady]);
|
||||
|
||||
return (
|
||||
// eslint-disable-next-line react/jsx-props-no-spreading
|
||||
<video ref={ref} autoPlay {...restProps} onPlaying={handlePlaying} />
|
||||
<video ref={ref} autoPlay {...restProps} {...otherBufferingHandlers} onPlaying={handlePlaying} />
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@ -26,8 +26,8 @@ type Target =
|
||||
'micro'
|
||||
| 'pictogram'
|
||||
| 'inline'
|
||||
| 'viewerPreview'
|
||||
| 'viewerFull'
|
||||
| 'preview'
|
||||
| 'full'
|
||||
| 'download';
|
||||
|
||||
export function getMessageContent(message: ApiMessage) {
|
||||
@ -191,9 +191,9 @@ export function getMessageMediaHash(
|
||||
return `${base}?size=m`;
|
||||
case 'inline':
|
||||
return !hasMessageLocalBlobUrl(message) ? getVideoOrAudioBaseHash(messageVideo, base) : undefined;
|
||||
case 'viewerPreview':
|
||||
return `${base}?size=m`;
|
||||
case 'viewerFull':
|
||||
case 'preview':
|
||||
return `${base}?size=x`;
|
||||
case 'full':
|
||||
return getVideoOrAudioBaseHash(messageVideo, base);
|
||||
case 'download':
|
||||
return `${base}?download`;
|
||||
@ -207,9 +207,9 @@ export function getMessageMediaHash(
|
||||
return `${base}?size=m`;
|
||||
case 'inline':
|
||||
return !hasMessageLocalBlobUrl(message) ? `${base}?size=x` : undefined;
|
||||
case 'viewerPreview':
|
||||
case 'preview':
|
||||
return `${base}?size=x`;
|
||||
case 'viewerFull':
|
||||
case 'full':
|
||||
case 'download':
|
||||
return `${base}?size=z`;
|
||||
}
|
||||
@ -220,13 +220,13 @@ export function getMessageMediaHash(
|
||||
case 'micro':
|
||||
case 'pictogram':
|
||||
case 'inline':
|
||||
case 'viewerPreview':
|
||||
case 'preview':
|
||||
if (!getDocumentHasPreview(document) || hasMessageLocalBlobUrl(message)) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return `${base}?size=m`;
|
||||
case 'viewerFull':
|
||||
case 'full':
|
||||
case 'download':
|
||||
return base;
|
||||
}
|
||||
@ -325,7 +325,7 @@ export function getMessageMediaFormat(
|
||||
}
|
||||
|
||||
if (fullVideo && IS_PROGRESSIVE_SUPPORTED && (
|
||||
target === 'viewerFull' || target === 'inline'
|
||||
target === 'full' || target === 'inline'
|
||||
)) {
|
||||
return ApiMediaFormat.Progressive;
|
||||
}
|
||||
|
||||
@ -14,7 +14,7 @@ const DEBOUNCE = 200;
|
||||
*/
|
||||
export type BufferedRange = { start: number; end: number };
|
||||
|
||||
const useBuffering = (noInitiallyBuffered = false) => {
|
||||
const useBuffering = (noInitiallyBuffered = false, onTimeUpdate?: AnyToVoidFunction) => {
|
||||
const [isBuffered, setIsBuffered] = useState(!noInitiallyBuffered);
|
||||
const [bufferedProgress, setBufferedProgress] = useState(0);
|
||||
const [bufferedRanges, setBufferedRanges] = useState<BufferedRange[]>([]);
|
||||
@ -24,6 +24,10 @@ const useBuffering = (noInitiallyBuffered = false) => {
|
||||
}, []);
|
||||
|
||||
const handleBuffering = useCallback<BufferingEvent>((e) => {
|
||||
if (e.type === 'timeupdate') {
|
||||
onTimeUpdate?.(e);
|
||||
}
|
||||
|
||||
const media = e.currentTarget as HTMLMediaElement;
|
||||
|
||||
if (!isSafariPatchInProgress(media)) {
|
||||
@ -36,7 +40,7 @@ const useBuffering = (noInitiallyBuffered = false) => {
|
||||
|
||||
setIsBufferedDebounced(media.readyState >= MIN_READY_STATE || media.currentTime > 0);
|
||||
}
|
||||
}, [setIsBufferedDebounced]);
|
||||
}, [onTimeUpdate, setIsBufferedDebounced]);
|
||||
|
||||
const bufferingHandlers = {
|
||||
onLoadedData: handleBuffering,
|
||||
|
||||
@ -15,8 +15,14 @@
|
||||
background-size: contain;
|
||||
}
|
||||
|
||||
.thumbnail ~ .thumbnail,
|
||||
.thumbnail ~ .full-media,
|
||||
.thumbnail {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.media-loading {
|
||||
position: absolute;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user