diff --git a/src/components/common/AnimatedSticker.tsx b/src/components/common/AnimatedSticker.tsx index 8fec37014..b74fee06c 100644 --- a/src/components/common/AnimatedSticker.tsx +++ b/src/components/common/AnimatedSticker.tsx @@ -16,6 +16,7 @@ import { IS_ELECTRON } from '../../util/windowEnvironment'; import useColorFilter from '../../hooks/stickers/useColorFilter'; import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps'; +import useFlag from '../../hooks/useFlag'; import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck'; import useLastCallback from '../../hooks/useLastCallback'; import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck'; @@ -96,6 +97,16 @@ const AnimatedSticker: FC = ({ const rgbColor = useRef<[number, number, number] | undefined>(); + const shouldForceOnHeavyAnimation = forceAlways || forceOnHeavyAnimation; + // Delay initialization until heavy animation ends + const [ + canInitialize, markCanInitialize, unmarkCanInitialize, + ] = useFlag(!isHeavyAnimating() || shouldForceOnHeavyAnimation); + useHeavyAnimationCheck(unmarkCanInitialize, markCanInitialize, shouldForceOnHeavyAnimation); + useEffect(() => { + if (shouldForceOnHeavyAnimation) markCanInitialize(); + }, [shouldForceOnHeavyAnimation]); + useSyncEffect(() => { if (color && !shouldUseColorFilter) { const { r, g, b } = hexToRgb(color); @@ -118,6 +129,7 @@ const AnimatedSticker: FC = ({ || isUnmountedRef.current || !tgsUrl || (sharedCanvas && (!sharedCanvasCoords || !sharedCanvas.offsetWidth || !sharedCanvas.offsetHeight)) + || (isHeavyAnimating() && !shouldForceOnHeavyAnimation) ) { return; } @@ -154,12 +166,13 @@ const AnimatedSticker: FC = ({ }); useEffect(() => { + if (!canInitialize) return; if (getRLottie()) { init(); } else { ensureRLottie().then(init); } - }, [init, tgsUrl, sharedCanvas, sharedCanvasCoords]); + }, [init, tgsUrl, sharedCanvas, sharedCanvasCoords, canInitialize]); const throttledInit = useThrottledCallback(init, [init], THROTTLE_MS); useSharedIntersectionObserver(sharedCanvas, throttledInit); @@ -239,7 +252,7 @@ const AnimatedSticker: FC = ({ } }, [playAnimation, animation, tgsUrl]); - useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || forceAlways || forceOnHeavyAnimation); + useHeavyAnimationCheck(pauseAnimation, playAnimation, !playKey || shouldForceOnHeavyAnimation); usePriorityPlaybackCheck(pauseAnimation, playAnimation, !playKey || forceAlways); // Pausing frame may not happen in background, // so we need to make sure it happens right after focusing, diff --git a/src/components/common/File.tsx b/src/components/common/File.tsx index 99e89bf82..5af734e3c 100644 --- a/src/components/common/File.tsx +++ b/src/components/common/File.tsx @@ -120,7 +120,7 @@ const File: FC = ({ {withThumb && ( )} diff --git a/src/components/common/GifButton.tsx b/src/components/common/GifButton.tsx index 24ea5d221..caa9a2420 100644 --- a/src/components/common/GifButton.tsx +++ b/src/components/common/GifButton.tsx @@ -154,7 +154,7 @@ const GifButton: FC = ({ {withThumb && ( diff --git a/src/components/common/MediaSpoiler.tsx b/src/components/common/MediaSpoiler.tsx index b8c455f8f..e530e6e6b 100644 --- a/src/components/common/MediaSpoiler.tsx +++ b/src/components/common/MediaSpoiler.tsx @@ -57,7 +57,12 @@ const MediaSpoiler: FC = ({ ref={ref} onClick={withAnimation ? handleClick : undefined} > - +
); diff --git a/src/components/common/ProfilePhoto.tsx b/src/components/common/ProfilePhoto.tsx index 9fdcbc35a..1863087e6 100644 --- a/src/components/common/ProfilePhoto.tsx +++ b/src/components/common/ProfilePhoto.tsx @@ -119,7 +119,7 @@ const ProfilePhoto: FC = ({ content = ( <> {isBlurredThumb ? ( - + ) : ( )} diff --git a/src/components/common/StickerView.tsx b/src/components/common/StickerView.tsx index da79fa017..9afddfa3e 100644 --- a/src/components/common/StickerView.tsx +++ b/src/components/common/StickerView.tsx @@ -9,7 +9,7 @@ import { getStickerPreviewHash } from '../../global/helpers'; import { selectIsAlwaysHighPriorityEmoji } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import * as mediaLoader from '../../util/mediaLoader'; -import { IS_ANDROID, IS_WEBM_SUPPORTED } from '../../util/windowEnvironment'; +import { IS_WEBM_SUPPORTED } from '../../util/windowEnvironment'; import useColorFilter from '../../hooks/stickers/useColorFilter'; import useCoordsInSharedCanvas from '../../hooks/useCoordsInSharedCanvas'; @@ -115,8 +115,8 @@ const StickerView: FC = ({ const fullMediaData = useMedia(fullMediaHash, !shouldLoad || shouldSkipFullMedia); // If Lottie data is loaded we will only render thumb if it's good enough (from preview) const [isPlayerReady, markPlayerReady] = useFlag(Boolean(isLottie && fullMediaData && !previewMediaData)); - // Delay mounting on Android until heavy animation ends - const [isReadyToMount, markReadyToMount, unmarkReadyToMount] = useFlag(!IS_ANDROID || !isHeavyAnimating()); + // Delay mounting until heavy animation ends + const [isReadyToMount, markReadyToMount, unmarkReadyToMount] = useFlag(!isHeavyAnimating()); useHeavyAnimationCheck(unmarkReadyToMount, markReadyToMount, isReadyToMount); const isFullMediaReady = isReadyToMount && fullMediaData && (isStatic || isPlayerReady); diff --git a/src/components/common/embedded/EmojiIconBackground.tsx b/src/components/common/embedded/EmojiIconBackground.tsx index 6ee00a67e..d0d0540d5 100644 --- a/src/components/common/embedded/EmojiIconBackground.tsx +++ b/src/components/common/embedded/EmojiIconBackground.tsx @@ -9,6 +9,9 @@ import { preloadImage } from '../../../util/files'; import { REM } from '../helpers/mediaDimensions'; import useDynamicColorListener from '../../../hooks/stickers/useDynamicColorListener'; +import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps'; +import useFlag from '../../../hooks/useFlag'; +import useHeavyAnimationCheck, { isHeavyAnimating } from '../../../hooks/useHeavyAnimationCheck'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useMedia from '../../../hooks/useMedia'; @@ -76,6 +79,10 @@ const EmojiIconBackground = ({ const lang = useLang(); + // Delay mounting until heavy animation ends + const [canUpdate, markCanUpdate, unmarkCanUpdate] = useFlag(!isHeavyAnimating()); + useHeavyAnimationCheck(unmarkCanUpdate, markCanUpdate); + const { customEmoji } = useCustomEmoji(emojiDocumentId); const previewMediaHash = customEmoji ? getStickerPreviewHash(customEmoji.id) : undefined; const previewUrl = useMedia(previewMediaHash); @@ -83,10 +90,10 @@ const EmojiIconBackground = ({ const customColor = useDynamicColorListener(containerRef); useEffect(() => { - if (!previewUrl) return; + if (!previewUrl || !canUpdate) return; preloadImage(previewUrl).then(setEmojiImage); - }, [previewUrl]); + }, [previewUrl, canUpdate]); const updateCanvas = useLastCallback(() => { const canvas = canvasRef.current; @@ -122,13 +129,15 @@ const EmojiIconBackground = ({ context.restore(); }); - useEffect(() => { + useEffectWithPrevDeps(([prevEmojiImage, prevLangRtl, prevCustomColor]) => { + // No need to trigger update if only `canUpdate` changed + if (emojiImage === prevEmojiImage && lang.isRtl === prevLangRtl && customColor === prevCustomColor) return; updateCanvas(); - }, [emojiImage, lang.isRtl, customColor]); + }, [emojiImage, lang.isRtl, customColor, canUpdate]); const updateCanvasSize = useLastCallback((parentWidth: number, parentHeight: number) => { const canvas = canvasRef.current; - if (!canvas) return; + if (!canvas || isHeavyAnimating()) return; canvas.width = parentWidth * dpr; canvas.height = parentHeight * dpr; @@ -147,18 +156,19 @@ const EmojiIconBackground = ({ }); }); - useResizeObserver(containerRef, handleResize); + useResizeObserver(containerRef, handleResize, !canUpdate); - useEffect(() => { + useEffectWithPrevDeps(([prevDpr]) => { + if (dpr === prevDpr) return; const container = containerRef.current; - if (!container) return; + if (!container || !canUpdate) return; const { width, height } = container.getBoundingClientRect(); requestMutation(() => { updateCanvasSize(width, height); }); - }, [dpr]); + }, [dpr, canUpdate]); return (
diff --git a/src/components/common/reactions/ReactionAnimatedEmoji.tsx b/src/components/common/reactions/ReactionAnimatedEmoji.tsx index 9f4d3e87e..844da0fee 100644 --- a/src/components/common/reactions/ReactionAnimatedEmoji.tsx +++ b/src/components/common/reactions/ReactionAnimatedEmoji.tsx @@ -36,6 +36,7 @@ type OwnProps = { shouldPause?: boolean; shouldLoop?: boolean; loopLimit?: number; + shouldDelayInit?: boolean; observeIntersection?: ObserveFn; }; @@ -66,6 +67,7 @@ const ReactionAnimatedEmoji = ({ shouldPause, shouldLoop, loopLimit, + shouldDelayInit, observeIntersection, }: OwnProps & StateProps) => { const { stopActiveReaction } = getActions(); @@ -167,7 +169,7 @@ const ReactionAnimatedEmoji = ({ size={size} noPlay={shouldPause} loopLimit={loopLimit} - forceAlways + forceAlways={!shouldDelayInit} observeIntersectionForPlaying={observeIntersection} /> )} @@ -179,7 +181,7 @@ const ReactionAnimatedEmoji = ({ tgsUrl={mediaDataCenterIcon} play={isIntersecting && !shouldPause} noLoop={!shouldLoop} - forceAlways + forceAlways={!shouldDelayInit} onLoad={markAnimationLoaded} onEnded={unmarkAnimationLoaded} /> @@ -193,7 +195,7 @@ const ReactionAnimatedEmoji = ({ tgsUrl={mediaDataEffect} play={isIntersecting} noLoop - forceAlways + forceAlways={!shouldDelayInit} onEnded={handleEnded} /> {isCustom && !assignedEffectId && isIntersecting && ( diff --git a/src/components/middle/message/BaseStory.tsx b/src/components/middle/message/BaseStory.tsx index 39f1fdbf9..dcaa74f18 100644 --- a/src/components/middle/message/BaseStory.tsx +++ b/src/components/middle/message/BaseStory.tsx @@ -83,7 +83,9 @@ function BaseStory({ className={fullClassName} onClick={isConnected ? handleClick : undefined} > - {!isExpired && isPreview && } + {!isExpired && isPreview && ( + + )} {shouldRender && ( <> = ({ noRecentReactors={isChannel} tags={tags} isCurrentUserPremium={isPremium} + getIsMessageListReady={getIsMessageListReady} /> ); } @@ -1475,6 +1476,7 @@ const Message: FC = ({ observeIntersection={observeIntersectionForPlaying} noRecentReactors={isChannel} tags={tags} + getIsMessageListReady={getIsMessageListReady} /> )}
diff --git a/src/components/middle/message/Photo.tsx b/src/components/middle/message/Photo.tsx index 90d9ae315..cb63c8e17 100644 --- a/src/components/middle/message/Photo.tsx +++ b/src/components/middle/message/Photo.tsx @@ -199,7 +199,9 @@ const Photo: FC = ({ style={style} onClick={isUploading ? undefined : handleClick} > - {withBlurredBackground && } + {withBlurredBackground && ( + + )} = ({ draggable={!isProtected} /> {withThumb && ( - + )} {isProtected && } {shouldRenderSpinner && !shouldRenderDownloadButton && ( diff --git a/src/components/middle/message/RoundVideo.tsx b/src/components/middle/message/RoundVideo.tsx index 198c6bc4b..b247c6a7b 100644 --- a/src/components/middle/message/RoundVideo.tsx +++ b/src/components/middle/message/RoundVideo.tsx @@ -260,7 +260,7 @@ const RoundVideo: FC = ({ {!shouldRenderSpoiler && ( )} diff --git a/src/components/middle/message/Video.tsx b/src/components/middle/message/Video.tsx index 3a40b638d..017ed0c9c 100644 --- a/src/components/middle/message/Video.tsx +++ b/src/components/middle/message/Video.tsx @@ -211,7 +211,9 @@ const Video: FC = ({ style={style} onClick={isUploading ? undefined : handleClick} > - {withBlurredBackground && } + {withBlurredBackground && ( + + )} {isInline && ( = ({ {hasThumb && !isPreviewPreloaded && ( )} {isProtected && } diff --git a/src/components/middle/message/reactions/ReactionButton.tsx b/src/components/middle/message/reactions/ReactionButton.tsx index 912a71172..638806b8d 100644 --- a/src/components/middle/message/reactions/ReactionButton.tsx +++ b/src/components/middle/message/reactions/ReactionButton.tsx @@ -1,4 +1,3 @@ -import type { FC } from '../../../../lib/teact/teact'; import React, { memo } from '../../../../lib/teact/teact'; import type { @@ -22,25 +21,29 @@ import styles from './ReactionButton.module.scss'; const REACTION_SIZE = 1.25 * REM; -const ReactionButton: FC<{ +type OwnProps = { reaction: ApiReactionCount; containerId: string; isOwnMessage?: boolean; recentReactors?: ApiPeer[]; className?: string; chosenClassName?: string; + shouldDelayInit?: boolean; observeIntersection?: ObserveFn; onClick?: (reaction: ApiReaction) => void; -}> = ({ +}; + +const ReactionButton = ({ reaction, containerId, isOwnMessage, recentReactors, className, chosenClassName, + shouldDelayInit, observeIntersection, onClick, -}) => { +}: OwnProps) => { const handleClick = useLastCallback(() => { onClick?.(reaction.reaction); }); @@ -63,6 +66,7 @@ const ReactionButton: FC<{ reaction={reaction.reaction} size={REACTION_SIZE} observeIntersection={observeIntersection} + shouldDelayInit={shouldDelayInit} /> {recentReactors?.length ? ( diff --git a/src/components/middle/message/reactions/Reactions.tsx b/src/components/middle/message/reactions/Reactions.tsx index d383ace47..fddf165ee 100644 --- a/src/components/middle/message/reactions/Reactions.tsx +++ b/src/components/middle/message/reactions/Reactions.tsx @@ -10,12 +10,14 @@ import type { ApiSavedReactionTag, } from '../../../../api/types'; import type { ObserveFn } from '../../../../hooks/useIntersectionObserver'; +import type { Signal } from '../../../../util/signals'; import { getReactionKey, isReactionChosen } from '../../../../global/helpers'; import { selectPeer } from '../../../../global/selectors'; import buildClassName from '../../../../util/buildClassName'; import { getMessageKey } from '../../../../util/messageKey'; +import useDerivedState from '../../../../hooks/useDerivedState'; import useLang from '../../../../hooks/useLang'; import useLastCallback from '../../../../hooks/useLastCallback'; @@ -33,6 +35,7 @@ type OwnProps = { isCurrentUserPremium?: boolean; observeIntersection?: ObserveFn; noRecentReactors?: boolean; + getIsMessageListReady: Signal; }; const MAX_RECENT_AVATARS = 3; @@ -46,6 +49,7 @@ const Reactions: FC = ({ noRecentReactors, isCurrentUserPremium, tags, + getIsMessageListReady, }) => { const { toggleReaction, @@ -61,6 +65,8 @@ const Reactions: FC = ({ results.reduce((acc, reaction) => acc + reaction.count, 0) ), [results]); + const isMessageListReady = useDerivedState(getIsMessageListReady); + const recentReactorsByReactionKey = useMemo(() => { const global = getGlobal(); @@ -149,6 +155,7 @@ const Reactions: FC = ({ onClick={handleClick} onRemove={handleRemoveReaction} observeIntersection={observeIntersection} + shouldDelayInit={!isMessageListReady} /> ) : ( = ({ reaction={reaction} onClick={handleClick} observeIntersection={observeIntersection} + shouldDelayInit={!isMessageListReady} /> ) ))} diff --git a/src/components/middle/message/reactions/SavedTagButton.tsx b/src/components/middle/message/reactions/SavedTagButton.tsx index 63c609606..957045289 100644 --- a/src/components/middle/message/reactions/SavedTagButton.tsx +++ b/src/components/middle/message/reactions/SavedTagButton.tsx @@ -1,4 +1,3 @@ -import type { FC } from '../../../../lib/teact/teact'; import React, { memo, useRef } from '../../../../lib/teact/teact'; import { getActions } from '../../../../global'; @@ -28,7 +27,7 @@ const REACTION_SIZE = 1.25 * REM; const TITLE_MAX_LENGTH = 15; const LOOP_LIMIT = 1; -const SavedTagButton: FC<{ +type OwnProps = { reaction: ApiReaction; tag?: ApiSavedReactionTag; containerId: string; @@ -39,10 +38,13 @@ const SavedTagButton: FC<{ chosenClassName?: string; isDisabled?: boolean; withContextMenu?: boolean; + shouldDelayInit?: boolean; observeIntersection?: ObserveFn; onClick?: (reaction: ApiReaction) => void; onRemove?: (reaction: ApiReaction) => void; -}> = ({ +}; + +const SavedTagButton = ({ reaction, tag, containerId, @@ -53,10 +55,11 @@ const SavedTagButton: FC<{ withCount, isDisabled, withContextMenu, + shouldDelayInit, observeIntersection, onClick, onRemove, -}) => { +}: OwnProps) => { const { editSavedReactionTag } = getActions(); // eslint-disable-next-line no-null/no-null const ref = useRef(null); @@ -138,6 +141,7 @@ const SavedTagButton: FC<{ loopLimit={LOOP_LIMIT} size={REACTION_SIZE} observeIntersection={observeIntersection} + shouldDelayInit={shouldDelayInit} /> {hasText && ( diff --git a/src/hooks/useCanvasBlur.ts b/src/hooks/useCanvasBlur.ts index 73183e208..7ed7a32a0 100644 --- a/src/hooks/useCanvasBlur.ts +++ b/src/hooks/useCanvasBlur.ts @@ -52,6 +52,8 @@ export default function useCanvasBlur( ctx.drawImage(img, -radius * 2, -radius * 2, width + radius * 4, height + radius * 4); + canvas.classList.remove('canvas-blur-setup'); + if (!IS_CANVAS_FILTER_SUPPORTED) { fastBlur(ctx, 0, 0, width, height, radius, ITERATIONS); } diff --git a/src/lib/lovely-chart/styles/_common.scss b/src/lib/lovely-chart/styles/_common.scss index b7f557721..a606dee17 100644 --- a/src/lib/lovely-chart/styles/_common.scss +++ b/src/lib/lovely-chart/styles/_common.scss @@ -8,9 +8,9 @@ -webkit-user-select: none; -webkit-tap-highlight-color: rgba(0, 0, 0, 0); - &.lovely-chart--state-invisible > * { - display: none; - } + // &.lovely-chart--state-invisible > * { + // display: none; + // } > canvas, .lovely-chart--tooltip canvas { diff --git a/src/styles/index.scss b/src/styles/index.scss index f280eb51e..6c7c39077 100644 --- a/src/styles/index.scss +++ b/src/styles/index.scss @@ -201,6 +201,10 @@ body:not(.is-ios) { } } +.canvas-blur-setup { + will-change: width, height; +} + .emoji-small { background: no-repeat; background-size: var(--emoji-size);