[Perf] Introduce Fasterdom and some performance fixes
This commit is contained in:
parent
925958d94e
commit
dba6963c34
@ -156,7 +156,9 @@ const App: FC<StateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
updateSizes();
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (IS_MULTITAB_SUPPORTED) return;
|
||||
|
||||
addActiveTabChangeListener(() => {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { ChangeEvent } from 'react';
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import monkeyPath from '../../assets/monkey.svg';
|
||||
|
||||
@ -149,7 +150,7 @@ const AuthPhoneNumber: FC<StateProps> = ({
|
||||
const isJustPastedRef = useRef(false);
|
||||
const handlePaste = useCallback(() => {
|
||||
isJustPastedRef.current = true;
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
isJustPastedRef.current = false;
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
useEffect, useRef, memo, useCallback,
|
||||
useEffect, useRef, memo, useCallback, useLayoutEffect,
|
||||
} from '../../lib/teact/teact';
|
||||
import { disableStrict, enableStrict } from '../../lib/fasterdom/stricterdom';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
@ -33,6 +34,7 @@ type StateProps =
|
||||
const DATA_PREFIX = 'tg://login?token=';
|
||||
const QR_SIZE = 280;
|
||||
const QR_PLANE_SIZE = 54;
|
||||
const QR_CODE_MUTATION_DURATION = 50; // The library is asynchronous and we need to wait for its mutation code
|
||||
|
||||
let qrCodeStylingPromise: Promise<typeof import('qr-code-styling')>;
|
||||
|
||||
@ -88,7 +90,7 @@ const AuthCode: FC<StateProps> = ({
|
||||
|
||||
const transitionClassNames = useMediaTransition(isQrMounted);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!authQrCode || !qrCode) {
|
||||
return () => {
|
||||
unmarkQrMounted();
|
||||
@ -102,6 +104,8 @@ const AuthCode: FC<StateProps> = ({
|
||||
const container = qrCodeRef.current!;
|
||||
const data = `${DATA_PREFIX}${authQrCode.token}`;
|
||||
|
||||
disableStrict();
|
||||
|
||||
qrCode.update({
|
||||
data,
|
||||
});
|
||||
@ -110,6 +114,11 @@ const AuthCode: FC<StateProps> = ({
|
||||
qrCode.append(container);
|
||||
markQrMounted();
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
enableStrict();
|
||||
}, QR_CODE_MUTATION_DURATION);
|
||||
|
||||
return undefined;
|
||||
}, [connectionState, authQrCode, isQrMounted, markQrMounted, unmarkQrMounted, qrCode]);
|
||||
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import React, {
|
||||
useEffect, useRef, memo, useCallback, useState, useMemo,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import { fastRaf } from '../../util/schedulers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
import generateIdFor from '../../util/generateIdFor';
|
||||
@ -13,7 +13,7 @@ import generateIdFor from '../../util/generateIdFor';
|
||||
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../hooks/useHeavyAnimationCheck';
|
||||
import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../hooks/usePriorityPlaybackCheck';
|
||||
import useBackgroundMode, { isBackgroundModeActive } from '../../hooks/useBackgroundMode';
|
||||
import useSyncEffect from '../../hooks/useSyncEffect';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import { useStateRef } from '../../hooks/useStateRef';
|
||||
|
||||
export type OwnProps = {
|
||||
@ -122,7 +122,8 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
|
||||
// Wait until element is properly mounted
|
||||
if (sharedCanvas && !sharedCanvas.offsetParent) {
|
||||
setTimeout(exec, ANIMATION_END_TIMEOUT);
|
||||
// `requestMeasure` is useful as timeouts are run in parallel with image loadings and thus causing reflow
|
||||
setTimeout(() => requestMeasure(exec), ANIMATION_END_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -156,7 +157,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
exec();
|
||||
} else {
|
||||
ensureLottie().then(() => {
|
||||
fastRaf(() => {
|
||||
requestMeasure(() => {
|
||||
if (containerRef.current) {
|
||||
exec();
|
||||
}
|
||||
@ -164,8 +165,8 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
});
|
||||
}
|
||||
}, [
|
||||
animation, renderId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop,
|
||||
viewId, sharedCanvas, sharedCanvasCoords,
|
||||
animation, renderId, tgsUrl, color, isLowPriority, noLoop, onLoad, quality, size, speed, onEnded, onLoop, viewId,
|
||||
sharedCanvas, sharedCanvasCoords,
|
||||
]);
|
||||
|
||||
useEffect(() => {
|
||||
@ -197,7 +198,7 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
}, [animation, playRef, playSegmentRef, viewId]);
|
||||
|
||||
const playAnimationOnRaf = useCallback(() => {
|
||||
fastRaf(playAnimation);
|
||||
requestMeasure(playAnimation);
|
||||
}, [playAnimation]);
|
||||
|
||||
const pauseAnimation = useCallback(() => {
|
||||
@ -206,13 +207,13 @@ const AnimatedSticker: FC<OwnProps> = ({
|
||||
}
|
||||
}, [animation, viewId]);
|
||||
|
||||
useSyncEffect(([prevNoLoop]) => {
|
||||
useEffectWithPrevDeps(([prevNoLoop]) => {
|
||||
if (prevNoLoop !== undefined && noLoop !== prevNoLoop) {
|
||||
animation?.setNoLoop(noLoop);
|
||||
}
|
||||
}, [noLoop, animation]);
|
||||
|
||||
useSyncEffect(([prevSharedCanvasCoords]) => {
|
||||
useEffectWithPrevDeps(([prevSharedCanvasCoords]) => {
|
||||
if (prevSharedCanvasCoords !== undefined && sharedCanvasCoords !== prevSharedCanvasCoords) {
|
||||
animation?.setSharedCanvasCoords(viewId, sharedCanvasCoords);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import React, {
|
||||
memo,
|
||||
useLayoutEffect,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
useState,
|
||||
@ -71,7 +71,7 @@ const ChatForumLastMessage: FC<OwnProps> = ({
|
||||
});
|
||||
}
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
const lastMessageElement = lastMessageRef.current;
|
||||
const mainColumnElement = mainColumnRef.current;
|
||||
if (!lastMessageElement || !mainColumnElement) return;
|
||||
|
||||
@ -35,10 +35,10 @@ import {
|
||||
} from '../../global/selectors';
|
||||
|
||||
import useAsyncRendering from '../right/hooks/useAsyncRendering';
|
||||
import { useIntersectionObserver } from '../../hooks/useIntersectionObserver';
|
||||
import useHorizontalScroll from '../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import { useStickerPickerObservers } from './hooks/useStickerPickerObservers';
|
||||
|
||||
import Loading from '../ui/Loading';
|
||||
import Button from '../ui/Button';
|
||||
@ -54,6 +54,7 @@ type OwnProps = {
|
||||
scrollHeaderRef?: RefObject<HTMLDivElement>;
|
||||
chatId?: string;
|
||||
className?: string;
|
||||
isHidden?: boolean;
|
||||
loadAndPlay: boolean;
|
||||
idPrefix?: string;
|
||||
withDefaultTopicIcons?: boolean;
|
||||
@ -84,9 +85,8 @@ type StateProps = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const SMOOTH_SCROLL_DISTANCE = 500;
|
||||
const SMOOTH_SCROLL_DISTANCE = 100;
|
||||
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||
const DEFAULT_ID_PREFIX = 'custom-emoji-set';
|
||||
const TOP_REACTIONS_COUNT = 16;
|
||||
const RECENT_REACTIONS_COUNT = 32;
|
||||
@ -99,12 +99,11 @@ const STICKER_SET_IDS_WITH_COVER = new Set([
|
||||
PREMIUM_STICKER_SET_ID,
|
||||
]);
|
||||
|
||||
const stickerSetIntersections: boolean[] = [];
|
||||
|
||||
const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
scrollContainerRef,
|
||||
scrollHeaderRef,
|
||||
className,
|
||||
isHidden,
|
||||
loadAndPlay,
|
||||
addedCustomEmojiIds,
|
||||
customEmojisById,
|
||||
@ -155,31 +154,12 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
: Object.values(pickTruthy(customEmojisById!, recentCustomEmojiIds!));
|
||||
}, [customEmojisById, isStatusPicker, recentCustomEmojiIds, recentStatusEmojis]);
|
||||
|
||||
const { observe: observeIntersection } = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
||||
}, (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const { id } = entry.target as HTMLDivElement;
|
||||
if (!id || !id.startsWith(idPrefix)) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Number(id.replace(`${idPrefix}-`, ''));
|
||||
stickerSetIntersections[index] = entry.isIntersecting;
|
||||
});
|
||||
|
||||
const intersectingWithIndexes = stickerSetIntersections
|
||||
.map((isIntersecting, index) => ({ index, isIntersecting }))
|
||||
.filter(({ isIntersecting }) => isIntersecting);
|
||||
|
||||
if (!intersectingWithIndexes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveSetIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index);
|
||||
});
|
||||
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: headerRef });
|
||||
const {
|
||||
observeIntersectionForSet,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForShowingItems,
|
||||
observeIntersectionForCovers,
|
||||
} = useStickerPickerObservers(containerRef, headerRef, idPrefix, setActiveSetIndex, isHidden);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -412,8 +392,10 @@ const CustomEmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||
index={i}
|
||||
idPrefix={idPrefix}
|
||||
observeIntersection={observeIntersection}
|
||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||
observeIntersection={observeIntersectionForSet}
|
||||
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
|
||||
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
|
||||
isNearActive={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isStatusPicker={isStatusPicker}
|
||||
isReactionPicker={isReactionPicker}
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useRef } from '../../lib/teact/teact';
|
||||
import React, { useCallback, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiUser, ApiMessage, ApiChat,
|
||||
@ -18,6 +18,7 @@ import { getPictogramDimensions } from './helpers/mediaDimensions';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useThumbnail from '../../hooks/useThumbnail';
|
||||
@ -71,6 +72,14 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
|
||||
const senderTitle = sender ? getSenderTitle(lang, sender) : message?.forwardInfo?.hiddenUserName;
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.type === 'mousedown' && e.button !== MouseButton.Main) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.();
|
||||
}, [onClick]);
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
@ -79,7 +88,8 @@ const EmbeddedMessage: FC<OwnProps> = ({
|
||||
className,
|
||||
sender && !noUserColors && `color-${getUserColorKey(sender)}`,
|
||||
)}
|
||||
onClick={message ? onClick : undefined}
|
||||
onClick={message && IS_TOUCH_ENV ? handleClick : undefined}
|
||||
onMouseDown={message && !IS_TOUCH_ENV ? handleClick : undefined}
|
||||
>
|
||||
{mediaThumbnail && renderPictogram(mediaThumbnail, mediaBlobUrl, isRoundVideo, isProtected, isSpoiler)}
|
||||
<div className="message-text">
|
||||
|
||||
@ -3,6 +3,7 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { MIN_PASSWORD_LENGTH } from '../../config';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
@ -72,7 +73,7 @@ const PasswordForm: FC<OwnProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
if (error) {
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
inputRef.current!.focus();
|
||||
inputRef.current!.select();
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback, useRef, useEffect, memo,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { isUserId } from '../../global/helpers';
|
||||
|
||||
@ -58,7 +59,7 @@ const Picker: FC<OwnProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
inputRef.current!.focus();
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
|
||||
@ -39,6 +39,7 @@ type OwnProps<T> = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
sharedCanvasRef?: React.RefObject<HTMLCanvasElement>;
|
||||
observeIntersection: ObserveFn;
|
||||
observeIntersectionForShowing?: ObserveFn;
|
||||
noShowPremium?: boolean;
|
||||
onClick?: (arg: OwnProps<T>['clickArg'], isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
clickArg: T;
|
||||
@ -69,6 +70,7 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
isStatusPicker,
|
||||
canViewSet,
|
||||
observeIntersection,
|
||||
observeIntersectionForShowing,
|
||||
isSelected,
|
||||
isCurrentUserPremium,
|
||||
noShowPremium,
|
||||
@ -100,6 +102,8 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
const shouldLoad = isIntersecting;
|
||||
const shouldPlay = isIntersecting && !noAnimate;
|
||||
|
||||
const isIntesectingForShowing = useIsIntersecting(ref, observeIntersectionForShowing);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
@ -287,19 +291,21 @@ const StickerButton = <T extends number | ApiSticker | ApiBotInlineMediaResult |
|
||||
onClick={handleClick}
|
||||
onContextMenu={handleContextMenu}
|
||||
>
|
||||
<StickerView
|
||||
containerRef={ref}
|
||||
sticker={sticker}
|
||||
isSmall
|
||||
size={size}
|
||||
shouldLoop
|
||||
shouldPreloadPreview
|
||||
noLoad={!shouldLoad}
|
||||
noPlay={!shouldPlay}
|
||||
withSharedAnimation
|
||||
sharedCanvasRef={sharedCanvasRef}
|
||||
customColor={customColor}
|
||||
/>
|
||||
{isIntesectingForShowing && (
|
||||
<StickerView
|
||||
containerRef={ref}
|
||||
sticker={sticker}
|
||||
isSmall
|
||||
size={size}
|
||||
shouldLoop
|
||||
shouldPreloadPreview
|
||||
noLoad={!shouldLoad}
|
||||
noPlay={!shouldPlay}
|
||||
withSharedAnimation
|
||||
sharedCanvasRef={sharedCanvasRef}
|
||||
customColor={customColor}
|
||||
/>
|
||||
)}
|
||||
{!noShowPremium && isLocked && (
|
||||
<div
|
||||
className="sticker-locked"
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal } from '../../global';
|
||||
|
||||
@ -41,7 +41,7 @@ type OwnProps = {
|
||||
loadAndPlay: boolean;
|
||||
index: number;
|
||||
idPrefix?: string;
|
||||
shouldRender: boolean;
|
||||
isNearActive: boolean;
|
||||
favoriteStickers?: ApiSticker[];
|
||||
isSavedMessages?: boolean;
|
||||
isStatusPicker?: boolean;
|
||||
@ -51,7 +51,9 @@ type OwnProps = {
|
||||
selectedReactionIds?: string[];
|
||||
withDefaultTopicIcon?: boolean;
|
||||
withDefaultStatusIcon?: boolean;
|
||||
observeIntersection: ObserveFn;
|
||||
observeIntersection?: ObserveFn;
|
||||
observeIntersectionForPlayingItems: ObserveFn;
|
||||
observeIntersectionForShowingItems: ObserveFn;
|
||||
availableReactions?: ApiAvailableReaction[];
|
||||
onStickerSelect?: (sticker: ApiSticker, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onReactionSelect?: (reaction: ApiReaction) => void;
|
||||
@ -74,7 +76,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
loadAndPlay,
|
||||
index,
|
||||
idPrefix,
|
||||
shouldRender,
|
||||
isNearActive,
|
||||
favoriteStickers,
|
||||
availableReactions,
|
||||
isSavedMessages,
|
||||
@ -86,6 +88,8 @@ const StickerSet: FC<OwnProps> = ({
|
||||
selectedReactionIds,
|
||||
withDefaultStatusIcon,
|
||||
observeIntersection,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForShowingItems,
|
||||
onReactionSelect,
|
||||
onStickerSelect,
|
||||
onStickerUnfave,
|
||||
@ -119,9 +123,11 @@ const StickerSet: FC<OwnProps> = ({
|
||||
|
||||
const [itemsPerRow, setItemsPerRow] = useState(getItemsPerRowFallback(windowWidth));
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection ?? observeIntersectionForShowingItems);
|
||||
const transitionClassNames = useMediaTransition(isIntersecting);
|
||||
|
||||
const transitionClassNames = useMediaTransition(shouldRender);
|
||||
// `isNearActive` is set in advance during animation, but it is not reliable for short sets
|
||||
const shouldRender = isNearActive || isIntersecting;
|
||||
|
||||
const stickerMarginPx = isMobile ? 8 : 16;
|
||||
const emojiMarginPx = isMobile ? 8 : 10;
|
||||
@ -194,13 +200,13 @@ const StickerSet: FC<OwnProps> = ({
|
||||
}, [calculateItemsPerRow]);
|
||||
useResizeObserver(ref, handleResize);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
if (!ref.current) return;
|
||||
setItemsPerRow(calculateItemsPerRow(ref.current.clientWidth));
|
||||
}, [calculateItemsPerRow]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isIntersecting && !stickerSet.stickers?.length && !stickerSet.reactions?.length && stickerSet.accessHash) {
|
||||
if (shouldRender && !stickerSet.stickers?.length && !stickerSet.reactions?.length && stickerSet.accessHash) {
|
||||
loadStickers({
|
||||
stickerSetInfo: {
|
||||
id: stickerSet.id,
|
||||
@ -208,7 +214,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
},
|
||||
});
|
||||
}
|
||||
}, [isIntersecting, loadStickers, stickerSet]);
|
||||
}, [shouldRender, loadStickers, stickerSet]);
|
||||
|
||||
const isLocked = !isSavedMessages && !isRecent && isEmoji && !isCurrentUserPremium
|
||||
&& stickerSet.stickers?.some(({ isFree }) => !isFree);
|
||||
@ -299,7 +305,7 @@ const StickerSet: FC<OwnProps> = ({
|
||||
isSelected={isSelected}
|
||||
loadAndPlay={loadAndPlay}
|
||||
availableReactions={availableReactions}
|
||||
observeIntersection={observeIntersection}
|
||||
observeIntersection={observeIntersectionForPlayingItems}
|
||||
onClick={onReactionSelect!}
|
||||
sharedCanvasRef={sharedCanvasRef}
|
||||
sharedCanvasHqRef={sharedCanvasHqRef}
|
||||
@ -321,7 +327,8 @@ const StickerSet: FC<OwnProps> = ({
|
||||
key={sticker.id}
|
||||
sticker={sticker}
|
||||
size={itemSize}
|
||||
observeIntersection={observeIntersection}
|
||||
observeIntersection={observeIntersectionForPlayingItems}
|
||||
observeIntersectionForShowing={observeIntersectionForShowingItems}
|
||||
noAnimate={!loadAndPlay}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isStatusPicker={isStatusPicker}
|
||||
|
||||
88
src/components/common/hooks/useStickerPickerObservers.ts
Normal file
88
src/components/common/hooks/useStickerPickerObservers.ts
Normal file
@ -0,0 +1,88 @@
|
||||
import type { RefObject } from 'react';
|
||||
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
import { useRef } from '../../../lib/teact/teact';
|
||||
import { ANIMATION_END_DELAY } from '../../../config';
|
||||
|
||||
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||
const STICKER_INTERSECTION_MARGIN = 100;
|
||||
const SLIDE_TRANSITION_DURATION = 350 + ANIMATION_END_DELAY;
|
||||
|
||||
export function useStickerPickerObservers(
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
headerRef: RefObject<HTMLDivElement>,
|
||||
idPrefix: string,
|
||||
setActiveSetIndex: (index: number) => void,
|
||||
isHidden?: boolean,
|
||||
) {
|
||||
const stickerSetIntersectionsRef = useRef<boolean[]>([]);
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForSet,
|
||||
freeze: freezeForSet,
|
||||
unfreeze: unfreezeForSet,
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
}, (entries) => {
|
||||
const stickerSetIntersections = stickerSetIntersectionsRef.current;
|
||||
|
||||
entries.forEach((entry) => {
|
||||
const index = Number(entry.target.id.replace(`${idPrefix}-`, ''));
|
||||
stickerSetIntersections[index] = entry.isIntersecting;
|
||||
});
|
||||
|
||||
const minIntersectingIndex = stickerSetIntersections.reduce((lowestIndex, isIntersecting, index) => {
|
||||
return isIntersecting && index < lowestIndex ? index : lowestIndex;
|
||||
}, Infinity);
|
||||
|
||||
if (minIntersectingIndex === Infinity) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveSetIndex(minIntersectingIndex);
|
||||
});
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForShowingItems,
|
||||
freeze: freezeForShowingItems,
|
||||
unfreeze: unfreezeForShowingItems,
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
||||
margin: STICKER_INTERSECTION_MARGIN,
|
||||
});
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForPlayingItems,
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
||||
margin: STICKER_INTERSECTION_MARGIN,
|
||||
});
|
||||
|
||||
const {
|
||||
observe: observeIntersectionForCovers,
|
||||
} = useIntersectionObserver({
|
||||
rootRef: headerRef,
|
||||
});
|
||||
|
||||
useSyncEffect(() => {
|
||||
if (isHidden) {
|
||||
freezeForSet();
|
||||
freezeForShowingItems();
|
||||
} else {
|
||||
setTimeout(() => {
|
||||
unfreezeForShowingItems();
|
||||
unfreezeForSet();
|
||||
}, SLIDE_TRANSITION_DURATION);
|
||||
}
|
||||
}, [freezeForSet, freezeForShowingItems, isHidden, unfreezeForSet, unfreezeForShowingItems]);
|
||||
|
||||
return {
|
||||
observeIntersectionForSet,
|
||||
observeIntersectionForShowingItems,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForCovers,
|
||||
};
|
||||
}
|
||||
@ -2,6 +2,7 @@ import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiChat } from '../../../api/types';
|
||||
@ -20,7 +21,6 @@ import { getOrderedTopics } from '../../../global/helpers';
|
||||
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
|
||||
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
||||
import { fastRaf } from '../../../util/schedulers';
|
||||
|
||||
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
||||
import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||
@ -147,13 +147,11 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (prevIsVisible !== isVisible) {
|
||||
// For performance reasons, we delay animation of the topic list panel to the next animation frame
|
||||
fastRaf(() => {
|
||||
requestNextMutation(() => {
|
||||
if (!ref.current) return;
|
||||
|
||||
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
||||
waitForTransitionEnd(ref.current, () => {
|
||||
dispatchHeavyAnimationStop();
|
||||
});
|
||||
waitForTransitionEnd(ref.current, dispatchHeavyAnimationStop);
|
||||
|
||||
onOpenAnimationStart?.();
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { useLayoutEffect, useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import { getGlobal } from '../../../../global';
|
||||
|
||||
import type { AnimationLevel } from '../../../../types';
|
||||
@ -24,7 +25,6 @@ import useLang from '../../../../hooks/useLang';
|
||||
import useEnsureMessage from '../../../../hooks/useEnsureMessage';
|
||||
import useMedia from '../../../../hooks/useMedia';
|
||||
import { ChatAnimationTypes } from './useChatAnimationType';
|
||||
import { fastRaf } from '../../../../util/schedulers';
|
||||
|
||||
import MessageSummary from '../../../common/MessageSummary';
|
||||
import ChatForumLastMessage from '../../../common/ChatForumLastMessage';
|
||||
@ -169,14 +169,14 @@ export default function useChatListEntry({
|
||||
if (animationType === ChatAnimationTypes.Opacity) {
|
||||
element.style.opacity = '0';
|
||||
|
||||
fastRaf(() => {
|
||||
requestMutation(() => {
|
||||
element.classList.add('animate-opacity');
|
||||
element.style.opacity = '1';
|
||||
});
|
||||
} else if (animationType === ChatAnimationTypes.Move) {
|
||||
element.style.transform = `translate3d(0, ${-orderDiff * CHAT_HEIGHT_PX}px, 0)`;
|
||||
|
||||
fastRaf(() => {
|
||||
requestMutation(() => {
|
||||
element.classList.add('animate-transform');
|
||||
element.style.transform = '';
|
||||
});
|
||||
@ -185,7 +185,7 @@ export default function useChatListEntry({
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
fastRaf(() => {
|
||||
requestMutation(() => {
|
||||
element.classList.remove('animate-opacity', 'animate-transform');
|
||||
element.style.opacity = '';
|
||||
element.style.transform = '';
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback, useRef, useEffect, memo,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../../../global';
|
||||
|
||||
import { isUserId } from '../../../../global/helpers';
|
||||
@ -66,7 +67,7 @@ const SettingsFoldersChatsPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
useEffect(() => {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
inputRef.current!.focus();
|
||||
});
|
||||
}, FOCUS_DELAY_MS);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import React, { memo, useCallback, useRef } from '../../lib/teact/teact';
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type { TabState } from '../../global/types';
|
||||
@ -162,7 +163,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
});
|
||||
confettiRef.current = confettiRef.current.filter((c) => !confettiToRemove.includes(c));
|
||||
if (confettiRef.current.length) {
|
||||
requestAnimationFrame(updateCanvas);
|
||||
requestMeasure(updateCanvas);
|
||||
} else {
|
||||
isRafStartedRef.current = false;
|
||||
}
|
||||
@ -175,7 +176,7 @@ const ConfettiContainer: FC<StateProps> = ({ confetti }) => {
|
||||
hideTimeout = setTimeout(forceUpdate, CONFETTI_FADEOUT_TIMEOUT);
|
||||
if (!isRafStartedRef.current) {
|
||||
isRafStartedRef.current = true;
|
||||
requestAnimationFrame(updateCanvas);
|
||||
requestMeasure(updateCanvas);
|
||||
}
|
||||
}
|
||||
return () => {
|
||||
|
||||
@ -3,6 +3,7 @@ import React, {
|
||||
useEffect, memo, useCallback, useState, useRef, useLayoutEffect,
|
||||
} from '../../lib/teact/teact';
|
||||
import { addExtraClass } from '../../lib/teact/teact-dom';
|
||||
import { requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { AnimationLevel, LangCode } from '../../types';
|
||||
@ -32,7 +33,6 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { waitForTransitionEnd } from '../../util/cssAnimationEndListeners';
|
||||
import { processDeepLink } from '../../util/deeplink';
|
||||
import { parseInitialLocationHash, parseLocationHash } from '../../util/routing';
|
||||
import { fastRaf } from '../../util/schedulers';
|
||||
import { Bundles, loadBundle } from '../../util/moduleLoader';
|
||||
import updateIcon from '../../util/updateIcon';
|
||||
|
||||
@ -393,7 +393,7 @@ const Main: FC<OwnProps & StateProps> = ({
|
||||
willAnimateLeftColumnRef.current = true;
|
||||
|
||||
if (IS_ANDROID) {
|
||||
fastRaf(() => {
|
||||
requestNextMutation(() => {
|
||||
document.body.classList.toggle('android-left-blackout-open', !isLeftColumnOpen);
|
||||
});
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef, useState,
|
||||
memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||
} from '../../lib/teact/teact';
|
||||
|
||||
import type { AnimationLevel, MediaViewerOrigin } from '../../types';
|
||||
@ -22,12 +22,12 @@ import useSignal from '../../hooks/useSignal';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
import { useFullscreenStatus } from '../../hooks/useFullscreen';
|
||||
import useZoomChange from './hooks/useZoomChangeSignal';
|
||||
import { useSignalRef } from '../../hooks/useSignalRef';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
|
||||
import MediaViewerContent from './MediaViewerContent';
|
||||
|
||||
import './MediaViewerSlides.scss';
|
||||
import { useSignalRef } from '../../hooks/useSignalRef';
|
||||
import useControlsSignal from './hooks/useControlsSignal';
|
||||
|
||||
const { easeOutCubic, easeOutQuart } = timingFunctions;
|
||||
|
||||
@ -143,7 +143,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
|
||||
useTimeout(() => setControlsVisible(true), ANIMATION_DURATION);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const { x, y, scale } = getTransform();
|
||||
lockControls(scale !== 1);
|
||||
if (leftSlideRef.current) {
|
||||
@ -251,7 +251,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
const calculateOffsetBoundaries = (
|
||||
{ x, y, scale }: Transform,
|
||||
offsetTop = 0,
|
||||
):[Transform, boolean, boolean] => {
|
||||
): [Transform, boolean, boolean] => {
|
||||
if (!initialContentRect) return [{ x, y, scale }, true, true];
|
||||
// Get current content boundaries
|
||||
let inBoundsX = true;
|
||||
@ -425,7 +425,7 @@ const MediaViewerSlides: FC<OwnProps> = ({
|
||||
content = activeSlideRef.current.querySelector('img, video');
|
||||
if (!content) return;
|
||||
// Store initial content rect, without transformations
|
||||
initialContentRect = content.getBoundingClientRect();
|
||||
initialContentRect = content!.getBoundingClientRect();
|
||||
}
|
||||
},
|
||||
onDrag: (event, captureEvent, {
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import type { ApiDimensions, ApiMessage } from '../../../api/types';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ApiDimensions, ApiMessage } from '../../../api/types';
|
||||
import { MediaViewerOrigin } from '../../../types';
|
||||
|
||||
import { ANIMATION_END_DELAY, MESSAGE_CONTENT_SELECTOR } from '../../../config';
|
||||
@ -62,27 +63,26 @@ export function animateOpening(
|
||||
const fromScaleX = fromWidth / toWidth;
|
||||
const fromScaleY = fromHeight / toHeight;
|
||||
|
||||
const ghost = createGhost(bestImageData || fromImage);
|
||||
applyStyles(ghost, {
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
width: `${toWidth}px`,
|
||||
height: `${toHeight}px`,
|
||||
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScaleX}, ${fromScaleY})`,
|
||||
});
|
||||
applyShape(ghost, origin);
|
||||
requestMutation(() => {
|
||||
const ghost = createGhost(bestImageData || fromImage);
|
||||
applyStyles(ghost, {
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
width: `${toWidth}px`,
|
||||
height: `${toHeight}px`,
|
||||
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScaleX}, ${fromScaleY})`,
|
||||
});
|
||||
applyShape(ghost, origin);
|
||||
|
||||
document.body.classList.add('ghost-animating');
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
document.body.appendChild(ghost);
|
||||
document.body.classList.add('ghost-animating');
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
ghost.style.transform = '';
|
||||
clearShape(ghost);
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
if (document.body.contains(ghost)) {
|
||||
document.body.removeChild(ghost);
|
||||
}
|
||||
@ -146,43 +146,41 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string,
|
||||
}
|
||||
|
||||
const existingGhost = document.getElementsByClassName('ghost')[0] as HTMLDivElement;
|
||||
|
||||
const ghost = existingGhost || createGhost(bestImageData || toImage, origin);
|
||||
if (!existingGhost) {
|
||||
applyStyles(ghost, {
|
||||
|
||||
let styles: Record<string, string>;
|
||||
if (existingGhost) {
|
||||
const {
|
||||
top, left, width, height,
|
||||
} = existingGhost.getBoundingClientRect();
|
||||
const scaleX = width / toWidth;
|
||||
const scaleY = height / toHeight;
|
||||
|
||||
styles = {
|
||||
transition: 'none',
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
transformOrigin: 'top left',
|
||||
transform: `translate3d(${left - toLeft}px, ${top - toTop}px, 0) scale(${scaleX}, ${scaleY})`,
|
||||
width: `${toWidth}px`,
|
||||
height: `${toHeight}px`,
|
||||
};
|
||||
} else {
|
||||
styles = {
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
width: `${toWidth}px`,
|
||||
height: `${toHeight}px`,
|
||||
transform: `translate3d(${fromTranslateX}px, ${fromTranslateY}px, 0) scale(${fromScaleX}, ${fromScaleY})`,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
if (existingGhost) {
|
||||
const {
|
||||
top,
|
||||
left,
|
||||
width,
|
||||
height,
|
||||
} = existingGhost.getBoundingClientRect();
|
||||
const scaleX = width / toWidth;
|
||||
const scaleY = height / toHeight;
|
||||
|
||||
applyStyles(ghost, {
|
||||
transition: 'none',
|
||||
top: `${toTop}px`,
|
||||
left: `${toLeft}px`,
|
||||
transformOrigin: 'top left',
|
||||
transform: `translate3d(${left - toLeft}px, ${top - toTop}px, 0) scale(${scaleX}, ${scaleY})`,
|
||||
width: `${toWidth}px`,
|
||||
height: `${toHeight}px`,
|
||||
});
|
||||
}
|
||||
document.body.classList.add('ghost-animating');
|
||||
requestMutation(() => {
|
||||
applyStyles(ghost, styles);
|
||||
if (!existingGhost) document.body.appendChild(ghost);
|
||||
document.body.classList.add('ghost-animating');
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
if (existingGhost) {
|
||||
existingGhost.style.transition = '';
|
||||
}
|
||||
@ -196,7 +194,7 @@ export function animateClosing(origin: MediaViewerOrigin, bestImageData: string,
|
||||
applyShape(ghost, origin);
|
||||
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
if (document.body.contains(ghost)) {
|
||||
document.body.removeChild(ghost);
|
||||
}
|
||||
|
||||
@ -5,6 +5,7 @@ import React, {
|
||||
useCallback,
|
||||
useState,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMeasure, requestNextMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { MessageListType } from '../../global/types';
|
||||
@ -154,9 +155,9 @@ const HeaderActions: FC<OwnProps & StateProps> = ({
|
||||
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
|
||||
searchInput.focus();
|
||||
} else if (noAnimation) {
|
||||
// The second RAF is necessary because teact must update the state and render the async component
|
||||
requestAnimationFrame(() => {
|
||||
requestAnimationFrame(setFocusInSearchInput);
|
||||
// The second RAF is necessary because Teact must update the state and render the async component
|
||||
requestMeasure(() => {
|
||||
requestNextMutation(setFocusInSearchInput);
|
||||
});
|
||||
} else {
|
||||
setTimeout(setFocusInSearchInput, SEARCH_FOCUS_DELAY_MS);
|
||||
|
||||
@ -10,7 +10,7 @@ import {
|
||||
getMessageMediaHash, getMessageSingleInlineButton,
|
||||
} from '../../global/helpers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
@ -39,7 +39,7 @@ type OwnProps = {
|
||||
customTitle?: string;
|
||||
className?: string;
|
||||
onUnpinMessage?: (id: number) => void;
|
||||
onClick?: (e: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
|
||||
onAllPinnedClick?: () => void;
|
||||
isLoading?: boolean;
|
||||
isFullWidth?: boolean;
|
||||
@ -78,6 +78,14 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
||||
|
||||
const [noHoverColor, markNoHoverColor, unmarkNoHoverColor] = useFlag();
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.type === 'mousedown' && e.button !== MouseButton.Main) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick?.(e);
|
||||
}, [onClick]);
|
||||
|
||||
function renderPictogram(thumbDataUri?: string, blobUrl?: string, spoiler?: boolean) {
|
||||
const { width, height } = getPictogramDimensions();
|
||||
const srcUrl = blobUrl || thumbDataUri;
|
||||
@ -140,7 +148,8 @@ const HeaderPinnedMessage: FC<OwnProps> = ({
|
||||
/>
|
||||
<div
|
||||
className={buildClassName(styles.pinnedMessage, noHoverColor && styles.noHover)}
|
||||
onClick={onClick}
|
||||
onClick={IS_TOUCH_ENV ? handleClick : undefined}
|
||||
onMouseDown={!IS_TOUCH_ENV ? handleClick : undefined}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
<PinnedMessageNavigation
|
||||
|
||||
@ -1,12 +1,18 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef,
|
||||
memo,
|
||||
useCallback,
|
||||
useEffect,
|
||||
useMemo,
|
||||
useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
import { requestForcedReflow, forceMeasure, requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
import type {
|
||||
ApiBotInfo, ApiMessage, ApiRestrictionReason, ApiTopic,
|
||||
} from '../../api/types';
|
||||
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
import type { MessageListType } from '../../global/types';
|
||||
import type { AnimationLevel } from '../../types';
|
||||
@ -47,7 +53,7 @@ import {
|
||||
} from '../../global/helpers';
|
||||
import { orderBy } from '../../util/iteratees';
|
||||
import { DPR } from '../../util/windowEnvironment';
|
||||
import { fastRaf, debounce, onTickEnd } from '../../util/schedulers';
|
||||
import { debounce, onTickEnd } from '../../util/schedulers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { groupMessages } from './helpers/groupMessages';
|
||||
import { preventMessageInputBlur } from './helpers/preventMessageInputBlur';
|
||||
@ -321,17 +327,15 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
onPinnedIntersectionChange({ hasScrolled: true });
|
||||
}
|
||||
|
||||
fastRaf(() => {
|
||||
if (!container.parentElement) {
|
||||
return;
|
||||
}
|
||||
if (!container.parentElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
scrollOffsetRef.current = container.scrollHeight - container.scrollTop;
|
||||
scrollOffsetRef.current = container.scrollHeight - container.scrollTop;
|
||||
|
||||
if (type === 'thread') {
|
||||
setScrollOffset({ chatId, threadId, scrollOffset: scrollOffsetRef.current });
|
||||
}
|
||||
});
|
||||
if (type === 'thread') {
|
||||
setScrollOffset({ chatId, threadId, scrollOffset: scrollOffsetRef.current });
|
||||
}
|
||||
});
|
||||
}, [
|
||||
updateStickyDates, hasTools, getForceNextPinnedInHeader, onPinnedIntersectionChange, type, chatId, threadId,
|
||||
@ -381,7 +385,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
useSyncEffect(
|
||||
() => rememberScrollPositionRef.current(),
|
||||
() => forceMeasure(() => rememberScrollPositionRef.current()),
|
||||
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
||||
[messageIds, isViewportNewest, hasTools, rememberScrollPositionRef],
|
||||
);
|
||||
@ -397,123 +401,133 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
const prevContainerHeight = prevContainerHeightRef.current;
|
||||
prevContainerHeightRef.current = containerHeight;
|
||||
|
||||
const container = containerRef.current!;
|
||||
listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item'));
|
||||
requestForcedReflow(() => {
|
||||
const container = containerRef.current!;
|
||||
listItemElementsRef.current = Array.from(container.querySelectorAll<HTMLDivElement>('.message-list-item'));
|
||||
|
||||
const hasLastMessageChanged = (
|
||||
messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1]
|
||||
);
|
||||
const hasViewportShifted = (
|
||||
messageIds?.[0] !== prevMessageIds?.[0] && messageIds?.length === (MESSAGE_LIST_SLICE / 2 + 1)
|
||||
);
|
||||
const wasMessageAdded = hasLastMessageChanged && !hasViewportShifted;
|
||||
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
|
||||
|
||||
// Add extra height when few messages to allow smooth scroll animation. Uses assumption that `parentElement`
|
||||
// is a Transition slide and its CSS class can not be reset in a declarative way.
|
||||
const shouldForceScroll = (
|
||||
isViewportNewest
|
||||
&& wasMessageAdded
|
||||
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
|
||||
&& !container.parentElement!.classList.contains('force-messages-scroll')
|
||||
&& (container.firstElementChild as HTMLDivElement)!.clientHeight <= container.offsetHeight * 2
|
||||
);
|
||||
|
||||
if (shouldForceScroll) {
|
||||
container.parentElement!.classList.add('force-messages-scroll');
|
||||
|
||||
setTimeout(() => {
|
||||
if (container.parentElement) {
|
||||
container.parentElement.classList.remove('force-messages-scroll');
|
||||
}
|
||||
}, MESSAGE_ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
const { scrollTop, scrollHeight, offsetHeight } = container;
|
||||
const scrollOffset = scrollOffsetRef.current;
|
||||
const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1];
|
||||
|
||||
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
|
||||
if (wasMessageAdded) {
|
||||
// If two new messages come at once (e.g. when bot responds) then the first message will update `scrollOffset`
|
||||
// right away (before animation) which creates inconsistency until the animation completes. To work around that,
|
||||
// we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate).
|
||||
const lastItemHeight = lastItemElement ? lastItemElement.offsetHeight : 0;
|
||||
bottomOffset -= lastItemHeight;
|
||||
}
|
||||
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
|
||||
|
||||
let newScrollTop!: number;
|
||||
|
||||
if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) {
|
||||
if (lastItemElement) {
|
||||
fastRaf(() => {
|
||||
fastSmoothScroll(
|
||||
container,
|
||||
lastItemElement,
|
||||
'end',
|
||||
BOTTOM_FOCUS_MARGIN,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
newScrollTop = scrollHeight - offsetHeight;
|
||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||
|
||||
// Scroll still needs to be restored after container resize
|
||||
if (!shouldForceScroll) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.APP_ENV === 'perf') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.time('scrollTop');
|
||||
}
|
||||
|
||||
const isResized = prevContainerHeight && prevContainerHeight !== containerHeight;
|
||||
const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`);
|
||||
const unreadDivider = (
|
||||
!anchor
|
||||
&& memoUnreadDividerBeforeIdRef.current
|
||||
&& container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`)
|
||||
);
|
||||
|
||||
if (isAtBottom && isResized) {
|
||||
if (isAnimatingScroll()) {
|
||||
return;
|
||||
}
|
||||
|
||||
newScrollTop = scrollHeight - offsetHeight;
|
||||
} else if (anchor) {
|
||||
const newAnchorTop = anchor.getBoundingClientRect().top;
|
||||
newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0));
|
||||
} else if (unreadDivider) {
|
||||
newScrollTop = Math.min(
|
||||
unreadDivider.offsetTop - (hasTools ? UNREAD_DIVIDER_TOP_WITH_TOOLS : UNREAD_DIVIDER_TOP),
|
||||
scrollHeight - scrollOffset,
|
||||
const hasLastMessageChanged = (
|
||||
messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1]
|
||||
);
|
||||
} else {
|
||||
newScrollTop = scrollHeight - scrollOffset;
|
||||
}
|
||||
const hasViewportShifted = (
|
||||
messageIds?.[0] !== prevMessageIds?.[0] && messageIds?.length === (MESSAGE_LIST_SLICE / 2 + 1)
|
||||
);
|
||||
const wasMessageAdded = hasLastMessageChanged && !hasViewportShifted;
|
||||
const isAlreadyFocusing = messageIds && memoFocusingIdRef.current === messageIds[messageIds.length - 1];
|
||||
|
||||
resetScroll(container, Math.ceil(newScrollTop));
|
||||
// Add extra height when few messages to allow smooth scroll animation. Uses assumption that `parentElement`
|
||||
// is a Transition slide and its CSS class can not be reset in a declarative way.
|
||||
const shouldForceScroll = (
|
||||
isViewportNewest
|
||||
&& wasMessageAdded
|
||||
&& (messageIds && messageIds.length < MESSAGE_LIST_SLICE / 2)
|
||||
&& !container.parentElement!.classList.contains('force-messages-scroll')
|
||||
&& (container.firstElementChild as HTMLDivElement)!.clientHeight <= container.offsetHeight * 2
|
||||
);
|
||||
|
||||
if (!memoFocusingIdRef.current) {
|
||||
isScrollTopJustUpdatedRef.current = true;
|
||||
fastRaf(() => {
|
||||
isScrollTopJustUpdatedRef.current = false;
|
||||
});
|
||||
}
|
||||
const {
|
||||
scrollTop,
|
||||
scrollHeight,
|
||||
offsetHeight,
|
||||
} = container;
|
||||
const scrollOffset = scrollOffsetRef.current;
|
||||
const lastItemElement = listItemElementsRef.current[listItemElementsRef.current.length - 1];
|
||||
|
||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||
let bottomOffset = scrollOffset - (prevContainerHeight || offsetHeight);
|
||||
if (wasMessageAdded) {
|
||||
// If two new messages come at once (e.g. when bot responds) then the first message will update `scrollOffset`
|
||||
// right away (before animation) which creates inconsistency until the animation completes. To work around that,
|
||||
// we calculate `isAtBottom` with a "buffer" of the latest message height (this is approximate).
|
||||
const lastItemHeight = lastItemElement ? lastItemElement.offsetHeight : 0;
|
||||
bottomOffset -= lastItemHeight;
|
||||
}
|
||||
const isAtBottom = isViewportNewest && prevIsViewportNewest && bottomOffset <= BOTTOM_THRESHOLD;
|
||||
|
||||
if (process.env.APP_ENV === 'perf') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.timeEnd('scrollTop');
|
||||
}
|
||||
let newScrollTop!: number;
|
||||
|
||||
if (wasMessageAdded && isAtBottom && !isAlreadyFocusing) {
|
||||
if (lastItemElement) {
|
||||
// Break out of `forceLayout`
|
||||
requestMeasure(() => {
|
||||
fastSmoothScroll(
|
||||
container,
|
||||
lastItemElement,
|
||||
'end',
|
||||
BOTTOM_FOCUS_MARGIN,
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
newScrollTop = scrollHeight - offsetHeight;
|
||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||
|
||||
// Scroll still needs to be restored after container resize
|
||||
if (!shouldForceScroll) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
if (process.env.APP_ENV === 'perf') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.time('scrollTop');
|
||||
}
|
||||
|
||||
const isResized = prevContainerHeight !== undefined && prevContainerHeight !== containerHeight;
|
||||
if (isResized && isAnimatingScroll()) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const anchor = anchorIdRef.current && container.querySelector(`#${anchorIdRef.current}`);
|
||||
const unreadDivider = (
|
||||
!anchor
|
||||
&& memoUnreadDividerBeforeIdRef.current
|
||||
&& container.querySelector<HTMLDivElement>(`.${UNREAD_DIVIDER_CLASS}`)
|
||||
);
|
||||
|
||||
if (isAtBottom && isResized) {
|
||||
newScrollTop = scrollHeight - offsetHeight;
|
||||
} else if (anchor) {
|
||||
const newAnchorTop = anchor.getBoundingClientRect().top;
|
||||
newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0));
|
||||
} else if (unreadDivider) {
|
||||
newScrollTop = Math.min(
|
||||
unreadDivider.offsetTop - (hasTools ? UNREAD_DIVIDER_TOP_WITH_TOOLS : UNREAD_DIVIDER_TOP),
|
||||
scrollHeight - scrollOffset,
|
||||
);
|
||||
} else {
|
||||
newScrollTop = scrollHeight - scrollOffset;
|
||||
}
|
||||
|
||||
return () => {
|
||||
if (shouldForceScroll) {
|
||||
container.parentElement!.classList.add('force-messages-scroll');
|
||||
|
||||
setTimeout(() => {
|
||||
if (container.parentElement) {
|
||||
container.parentElement.classList.remove('force-messages-scroll');
|
||||
}
|
||||
}, MESSAGE_ANIMATION_DURATION);
|
||||
}
|
||||
|
||||
resetScroll(container, Math.ceil(newScrollTop));
|
||||
|
||||
if (!memoFocusingIdRef.current) {
|
||||
isScrollTopJustUpdatedRef.current = true;
|
||||
|
||||
requestMeasure(() => {
|
||||
isScrollTopJustUpdatedRef.current = false;
|
||||
});
|
||||
}
|
||||
|
||||
scrollOffsetRef.current = Math.max(Math.ceil(scrollHeight - newScrollTop), offsetHeight);
|
||||
|
||||
if (process.env.APP_ENV === 'perf') {
|
||||
// eslint-disable-next-line no-console
|
||||
console.timeEnd('scrollTop');
|
||||
}
|
||||
};
|
||||
});
|
||||
// This should match deps for `useSyncEffect` above
|
||||
}, [messageIds, isViewportNewest, getContainerHeight, prevContainerHeightRef, hasTools]);
|
||||
}, [messageIds, isViewportNewest, hasTools, getContainerHeight, prevContainerHeightRef]);
|
||||
|
||||
useEffectWithPrevDeps(([prevIsSelectModeActive]) => {
|
||||
if (prevIsSelectModeActive !== undefined) {
|
||||
@ -631,6 +645,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
isChannelChat={isChannelChat}
|
||||
messageIds={messageIds || [lastMessage!.id]}
|
||||
messageGroups={messageGroups || groupMessages([lastMessage!])}
|
||||
getContainerHeight={getContainerHeight}
|
||||
isViewportNewest={Boolean(isViewportNewest)}
|
||||
isUnread={Boolean(firstUnreadId)}
|
||||
withUsers={withUsers}
|
||||
|
||||
@ -5,6 +5,7 @@ import { getActions } from '../../global';
|
||||
|
||||
import type { MessageListType } from '../../global/types';
|
||||
import type { PinnedIntersectionChangedCallback } from './hooks/usePinnedMessage';
|
||||
import type { Signal } from '../../util/signals';
|
||||
|
||||
import { SCHEDULED_WHEN_ONLINE } from '../../config';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
@ -31,6 +32,7 @@ interface OwnProps {
|
||||
threadId: number;
|
||||
messageIds: number[];
|
||||
messageGroups: MessageDateGroup[];
|
||||
getContainerHeight: Signal<number | undefined>;
|
||||
isViewportNewest: boolean;
|
||||
isUnread: boolean;
|
||||
withUsers: boolean;
|
||||
@ -60,6 +62,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
threadId,
|
||||
messageIds,
|
||||
messageGroups,
|
||||
getContainerHeight,
|
||||
isViewportNewest,
|
||||
isUnread,
|
||||
isComments,
|
||||
@ -96,6 +99,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
type,
|
||||
containerRef,
|
||||
messageIds,
|
||||
getContainerHeight,
|
||||
isViewportNewest,
|
||||
isUnread,
|
||||
onFabToggle,
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useEffect, useState, memo, useMemo, useCallback,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChat, ApiChatBannedRights } from '../../api/types';
|
||||
@ -27,7 +28,7 @@ import {
|
||||
GENERAL_TOPIC_ID,
|
||||
TMP_CHAT_ID,
|
||||
} from '../../config';
|
||||
import { MASK_IMAGE_DISABLED } from '../../util/windowEnvironment';
|
||||
import { IS_ANDROID, IS_IOS, MASK_IMAGE_DISABLED } from '../../util/windowEnvironment';
|
||||
import { DropAreaState } from './composer/DropArea';
|
||||
import {
|
||||
selectChat,
|
||||
@ -277,17 +278,21 @@ const MiddleColumn: FC<OwnProps & StateProps> = ({
|
||||
|
||||
// Fix for mobile virtual keyboard
|
||||
useEffect(() => {
|
||||
if (!IS_IOS && !IS_ANDROID) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const { visualViewport } = window;
|
||||
if (!visualViewport) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const handleResize = () => {
|
||||
if (visualViewport.height !== document.documentElement.clientHeight) {
|
||||
document.body.classList.add('keyboard-visible');
|
||||
} else {
|
||||
document.body.classList.remove('keyboard-visible');
|
||||
}
|
||||
const isFixNeeded = visualViewport.height !== document.documentElement.clientHeight;
|
||||
|
||||
requestMutation(() => {
|
||||
document.body.classList.toggle('keyboard-visible', isFixNeeded);
|
||||
});
|
||||
};
|
||||
|
||||
visualViewport.addEventListener('resize', handleResize);
|
||||
|
||||
@ -1,7 +1,8 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef,
|
||||
memo, useCallback, useEffect, useLayoutEffect, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState, MessageListType } from '../../global/types';
|
||||
@ -289,7 +290,7 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
|| (shouldRenderAudioPlayer && renderingAudioMessage);
|
||||
|
||||
// Logic for transition to and from custom display of AudioPlayer/PinnedMessage on smaller screens
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const componentEl = componentRef.current;
|
||||
if (!componentEl) {
|
||||
return;
|
||||
@ -309,7 +310,9 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
|
||||
// Remove animation class to prevent it messing up the show transitions
|
||||
setTimeout(() => {
|
||||
componentEl.classList.remove('animated');
|
||||
requestMutation(() => {
|
||||
componentEl.classList.remove('animated');
|
||||
});
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
componentEl.classList.remove('tools-stacked');
|
||||
|
||||
@ -2,10 +2,12 @@ import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef, useState, useLayoutEffect,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChat } from '../../api/types';
|
||||
|
||||
import { IS_IOS } from '../../util/windowEnvironment';
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import {
|
||||
selectCurrentTextSearch,
|
||||
@ -69,12 +71,17 @@ const MobileSearchFooter: FC<StateProps> = ({
|
||||
const { activeElement } = document;
|
||||
if (activeElement && (activeElement === inputRef.current)) {
|
||||
const { pageTop, height } = visualViewport;
|
||||
mainEl.style.transform = `translateY(${pageTop}px)`;
|
||||
mainEl.style.height = `${height}px`;
|
||||
document.documentElement.scrollTop = pageTop;
|
||||
|
||||
requestMutation(() => {
|
||||
mainEl.style.transform = `translateY(${pageTop}px)`;
|
||||
mainEl.style.height = `${height}px`;
|
||||
document.documentElement.scrollTop = pageTop;
|
||||
});
|
||||
} else {
|
||||
mainEl.style.transform = '';
|
||||
mainEl.style.height = '';
|
||||
requestMutation(() => {
|
||||
mainEl.style.transform = '';
|
||||
mainEl.style.height = '';
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@ -96,14 +103,12 @@ const MobileSearchFooter: FC<StateProps> = ({
|
||||
}, [chat?.id, focusMessage, foundIds, threadId]);
|
||||
|
||||
// Disable native up/down buttons on iOS
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!IS_IOS) return;
|
||||
|
||||
Array.from(document.querySelectorAll<HTMLInputElement>('input')).forEach((input) => {
|
||||
input.disabled = Boolean(isActive && input !== inputRef.current);
|
||||
});
|
||||
|
||||
Array.from(document.querySelectorAll<HTMLDivElement>('div[contenteditable]')).forEach((div) => {
|
||||
div.contentEditable = isActive ? 'false' : 'true';
|
||||
});
|
||||
}, [isActive]);
|
||||
|
||||
// Blur on exit
|
||||
@ -113,7 +118,7 @@ const MobileSearchFooter: FC<StateProps> = ({
|
||||
}
|
||||
}, [isActive]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
useEffect(() => {
|
||||
const searchInput = document.querySelector<HTMLInputElement>('#MobileSearch input')!;
|
||||
searchInput.blur();
|
||||
}, [isHistoryCalendarOpen]);
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useRef,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useMemo,
|
||||
memo,
|
||||
} from '../../lib/teact/teact';
|
||||
@ -27,7 +27,7 @@ const PinnedMessageNavigation: FC<OwnProps> = ({
|
||||
return calculateMarkup(count, index);
|
||||
}, [count, index]);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!containerRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
@ -360,7 +361,10 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
if (!mainButton || !input) return;
|
||||
|
||||
const { width } = mainButton.getBoundingClientRect();
|
||||
input.style.setProperty('--margin-for-scrollbar', `${width}px`);
|
||||
|
||||
requestMutation(() => {
|
||||
input.style.setProperty('--margin-for-scrollbar', `${width}px`);
|
||||
});
|
||||
}, [lang, isOpen]);
|
||||
|
||||
const MoreMenuButton: FC<{ onTrigger: () => void; isOpen?: boolean }> = useMemo(() => {
|
||||
|
||||
@ -2,6 +2,7 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMeasure, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
@ -532,7 +533,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
setHtml(`${getHtml()}${newHtml}`);
|
||||
|
||||
// If selection is outside of input, set cursor at the end of input
|
||||
requestAnimationFrame(() => {
|
||||
requestNextMutation(() => {
|
||||
focusEditableElement(messageInput);
|
||||
});
|
||||
}, [isComposerBlocked, getHtml, setHtml]);
|
||||
@ -755,7 +756,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
clearDraft({ chatId, localOnly: true });
|
||||
|
||||
// Wait until message animation starts
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer();
|
||||
});
|
||||
}, [
|
||||
@ -842,7 +843,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
// Wait until message animation starts
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer();
|
||||
});
|
||||
}, [
|
||||
@ -906,7 +907,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
if (requestedDraftText) {
|
||||
setHtml(requestedDraftText);
|
||||
resetOpenChatWithDraft();
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
requestNextMutation(() => {
|
||||
const messageInput = document.getElementById(EDITABLE_INPUT_ID)!;
|
||||
focusEditableElement(messageInput, true);
|
||||
});
|
||||
@ -939,13 +941,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
requestCalendar((scheduledAt) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleMessageSchedule({ gif, isSilent }, scheduledAt);
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendMessage({ gif, isSilent });
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer(true);
|
||||
});
|
||||
}
|
||||
@ -971,13 +973,13 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
requestCalendar((scheduledAt) => {
|
||||
cancelForceShowSymbolMenu();
|
||||
handleMessageSchedule({ sticker, isSilent }, scheduledAt);
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer(shouldPreserveInput);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
sendMessage({ sticker, isSilent, shouldUpdateStickerSetsOrder });
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer(shouldPreserveInput);
|
||||
});
|
||||
}
|
||||
@ -1015,7 +1017,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
clearDraft({ chatId, localOnly: true });
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer();
|
||||
});
|
||||
}, [
|
||||
@ -1025,7 +1027,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handleBotCommandSelect = useCallback(() => {
|
||||
clearDraft({ chatId, localOnly: true });
|
||||
requestAnimationFrame(() => {
|
||||
requestMeasure(() => {
|
||||
resetComposer();
|
||||
});
|
||||
}, [chatId, clearDraft, resetComposer]);
|
||||
@ -1519,6 +1521,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
className={buildClassName(mainButtonState, !isReady && 'not-ready', activeVoiceRecording && 'recording')}
|
||||
disabled={areVoiceMessagesNotAllowed}
|
||||
allowDisabledClick
|
||||
noFastClick
|
||||
ariaLabel={lang(sendButtonAriaLabel)}
|
||||
onClick={mainButtonHandler}
|
||||
onContextMenu={
|
||||
|
||||
@ -53,8 +53,7 @@ const ICONS_BY_CATEGORY: Record<string, string> = {
|
||||
};
|
||||
|
||||
const OPEN_ANIMATION_DELAY = 200;
|
||||
// Only a few categories are above this height.
|
||||
const SMOOTH_SCROLL_DISTANCE = 800;
|
||||
const SMOOTH_SCROLL_DISTANCE = 100;
|
||||
const FOCUS_MARGIN = 50;
|
||||
const HEADER_BUTTON_WIDTH = 42; // px. Includes margins
|
||||
const INTERSECTION_THROTTLE = 200;
|
||||
@ -94,15 +93,15 @@ const EmojiPicker: FC<OwnProps & StateProps> = ({
|
||||
categoryIntersections[index] = entry.isIntersecting;
|
||||
});
|
||||
|
||||
const intersectingWithIndexes = categoryIntersections
|
||||
.map((isIntersecting, index) => ({ index, isIntersecting }))
|
||||
.filter(({ isIntersecting }) => isIntersecting);
|
||||
const minIntersectingIndex = categoryIntersections.reduce((lowestIndex, isIntersecting, index) => {
|
||||
return isIntersecting && index < lowestIndex ? index : lowestIndex;
|
||||
}, Infinity);
|
||||
|
||||
if (!intersectingWithIndexes.length) {
|
||||
if (minIntersectingIndex === Infinity) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveCategoryIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index);
|
||||
setActiveCategoryIndex(minIntersectingIndex);
|
||||
});
|
||||
|
||||
const canRenderContents = useAsyncRendering([], MENU_TRANSITION_DURATION);
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useRef,
|
||||
memo, useCallback, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiSticker } from '../../../api/types';
|
||||
@ -10,6 +10,7 @@ import findInViewport from '../../../util/findInViewport';
|
||||
import isFullyVisible from '../../../util/isFullyVisible';
|
||||
import fastSmoothScrollHorizontal from '../../../util/fastSmoothScrollHorizontal';
|
||||
|
||||
import useEffectWithPrevDeps from '../../../hooks/useEffectWithPrevDeps';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import usePrevDuringAnimation from '../../../hooks/usePrevDuringAnimation';
|
||||
import { useKeyboardNavigation } from './hooks/useKeyboardNavigation';
|
||||
@ -126,7 +127,11 @@ const EmojiTooltip: FC<OwnProps> = ({
|
||||
onClose,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
useEffectWithPrevDeps(([prevSelectedIndex]) => {
|
||||
if (prevSelectedIndex === undefined || prevSelectedIndex === -1) {
|
||||
return;
|
||||
}
|
||||
|
||||
setItemVisible(selectedIndex, containerRef);
|
||||
}, [selectedIndex]);
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
useEffect, useRef, memo, useState, useCallback, useLayoutEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMutation, requestForcedReflow } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { IAnchorPosition, ISettings } from '../../../types';
|
||||
@ -159,36 +160,41 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
);
|
||||
|
||||
const maxInputHeight = isMobile ? 256 : 416;
|
||||
const maxInputHeight = isAttachmentModalInput ? MAX_ATTACHMENT_MODAL_INPUT_HEIGHT : (isMobile ? 256 : 416);
|
||||
const updateInputHeight = useCallback((willSend = false) => {
|
||||
const scroller = inputRef.current!.closest<HTMLDivElement>(`.${SCROLLER_CLASS}`)!;
|
||||
const clone = scrollerCloneRef.current!;
|
||||
const currentHeight = Number(scroller.style.height.replace('px', ''));
|
||||
const maxHeight = isAttachmentModalInput ? MAX_ATTACHMENT_MODAL_INPUT_HEIGHT : maxInputHeight;
|
||||
const newHeight = Math.min(clone.scrollHeight, maxHeight);
|
||||
if (newHeight === currentHeight) {
|
||||
return;
|
||||
}
|
||||
requestForcedReflow(() => {
|
||||
const scroller = inputRef.current!.closest<HTMLDivElement>(`.${SCROLLER_CLASS}`)!;
|
||||
const currentHeight = Number(scroller.style.height.replace('px', ''));
|
||||
const clone = scrollerCloneRef.current!;
|
||||
const { scrollHeight } = clone;
|
||||
const newHeight = Math.min(scrollHeight, maxInputHeight);
|
||||
|
||||
const transitionDuration = Math.round(
|
||||
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
|
||||
);
|
||||
if (newHeight === currentHeight) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
const exec = () => {
|
||||
scroller.style.height = `${newHeight}px`;
|
||||
scroller.style.transitionDuration = `${transitionDuration}ms`;
|
||||
scroller.classList.toggle('overflown', clone.scrollHeight > maxHeight);
|
||||
};
|
||||
const isOverflown = scrollHeight > maxInputHeight;
|
||||
|
||||
if (willSend) {
|
||||
// Sync with sending animation
|
||||
requestAnimationFrame(exec);
|
||||
} else {
|
||||
exec();
|
||||
}
|
||||
}, [isAttachmentModalInput, maxInputHeight]);
|
||||
function exec() {
|
||||
const transitionDuration = Math.round(
|
||||
TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)),
|
||||
);
|
||||
scroller.style.height = `${newHeight}px`;
|
||||
scroller.style.transitionDuration = `${transitionDuration}ms`;
|
||||
scroller.classList.toggle('overflown', isOverflown);
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (willSend) {
|
||||
// Delay to next frame to sync with sending animation
|
||||
requestMutation(exec);
|
||||
return undefined;
|
||||
} else {
|
||||
return exec;
|
||||
}
|
||||
});
|
||||
}, [maxInputHeight]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isAttachmentModalInput) return;
|
||||
updateInputHeight(false);
|
||||
}, [isAttachmentModalInput, updateInputHeight]);
|
||||
@ -481,7 +487,7 @@ const MessageInput: FC<OwnProps & StateProps> = ({
|
||||
const captureFirstTab = debounce((e: KeyboardEvent) => {
|
||||
if (e.key === 'Tab' && !getIsDirectTextInputDisabled()) {
|
||||
e.preventDefault();
|
||||
requestAnimationFrame(focusInput);
|
||||
requestMutation(focusInput);
|
||||
}
|
||||
}, TAB_INDEX_PRIORITY_TIMEOUT, true, false);
|
||||
|
||||
|
||||
@ -3,6 +3,7 @@ import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ApiNewPoll } from '../../../api/types';
|
||||
|
||||
@ -85,7 +86,8 @@ const PollModal: FC<OwnProps> = ({
|
||||
|
||||
const addNewOption = useCallback((newOptions: string[] = []) => {
|
||||
setOptions([...newOptions, '']);
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
requestNextMutation(() => {
|
||||
const list = optionsListRef.current;
|
||||
if (!list) {
|
||||
return;
|
||||
@ -189,7 +191,7 @@ const PollModal: FC<OwnProps> = ({
|
||||
}
|
||||
}
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestNextMutation(() => {
|
||||
if (!optionsListRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -24,10 +24,10 @@ import { pickTruthy, uniqueByField } from '../../../util/iteratees';
|
||||
import { selectChat, selectIsChatWithSelf, selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||
|
||||
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
|
||||
import { useIntersectionObserver } from '../../../hooks/useIntersectionObserver';
|
||||
import useHorizontalScroll from '../../../hooks/useHorizontalScroll';
|
||||
import useLang from '../../../hooks/useLang';
|
||||
import useSendMessageAction from '../../../hooks/useSendMessageAction';
|
||||
import { useStickerPickerObservers } from '../../common/hooks/useStickerPickerObservers';
|
||||
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Loading from '../../ui/Loading';
|
||||
@ -43,6 +43,7 @@ type OwnProps = {
|
||||
chatId: string;
|
||||
threadId?: number;
|
||||
className: string;
|
||||
isHidden?: boolean;
|
||||
loadAndPlay: boolean;
|
||||
canSendStickers?: boolean;
|
||||
onStickerSelect: (
|
||||
@ -62,16 +63,14 @@ type StateProps = {
|
||||
isCurrentUserPremium?: boolean;
|
||||
};
|
||||
|
||||
const SMOOTH_SCROLL_DISTANCE = 500;
|
||||
const SMOOTH_SCROLL_DISTANCE = 100;
|
||||
const HEADER_BUTTON_WIDTH = 52; // px (including margin)
|
||||
const STICKER_INTERSECTION_THROTTLE = 200;
|
||||
|
||||
const stickerSetIntersections: boolean[] = [];
|
||||
|
||||
const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
chat,
|
||||
threadId,
|
||||
className,
|
||||
isHidden,
|
||||
loadAndPlay,
|
||||
canSendStickers,
|
||||
recentStickers,
|
||||
@ -103,31 +102,12 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const sendMessageAction = useSendMessageAction(chat!.id, threadId);
|
||||
|
||||
const { observe: observeIntersection } = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
throttleMs: STICKER_INTERSECTION_THROTTLE,
|
||||
}, (entries) => {
|
||||
entries.forEach((entry) => {
|
||||
const { id } = entry.target as HTMLDivElement;
|
||||
if (!id || !id.startsWith('sticker-set-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
const index = Number(id.replace('sticker-set-', ''));
|
||||
stickerSetIntersections[index] = entry.isIntersecting;
|
||||
});
|
||||
|
||||
const intersectingWithIndexes = stickerSetIntersections
|
||||
.map((isIntersecting, index) => ({ index, isIntersecting }))
|
||||
.filter(({ isIntersecting }) => isIntersecting);
|
||||
|
||||
if (!intersectingWithIndexes.length) {
|
||||
return;
|
||||
}
|
||||
|
||||
setActiveSetIndex(intersectingWithIndexes[Math.floor(intersectingWithIndexes.length / 2)].index);
|
||||
});
|
||||
const { observe: observeIntersectionForCovers } = useIntersectionObserver({ rootRef: headerRef });
|
||||
const {
|
||||
observeIntersectionForSet,
|
||||
observeIntersectionForPlayingItems,
|
||||
observeIntersectionForShowingItems,
|
||||
observeIntersectionForCovers,
|
||||
} = useStickerPickerObservers(containerRef, headerRef, 'sticker-set', setActiveSetIndex, isHidden);
|
||||
|
||||
const lang = useLang();
|
||||
|
||||
@ -366,8 +346,10 @@ const StickerPicker: FC<OwnProps & StateProps> = ({
|
||||
stickerSet={stickerSet}
|
||||
loadAndPlay={Boolean(canAnimate && loadAndPlay)}
|
||||
index={i}
|
||||
observeIntersection={observeIntersection}
|
||||
shouldRender={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||
observeIntersection={observeIntersectionForSet}
|
||||
observeIntersectionForPlayingItems={observeIntersectionForPlayingItems}
|
||||
observeIntersectionForShowingItems={observeIntersectionForShowingItems}
|
||||
isNearActive={activeSetIndex >= i - 1 && activeSetIndex <= i + 1}
|
||||
favoriteStickers={favoriteStickers}
|
||||
isSavedMessages={isSavedMessages}
|
||||
isCurrentUserPremium={isCurrentUserPremium}
|
||||
|
||||
@ -24,11 +24,7 @@
|
||||
padding-right: env(safe-area-inset-right);
|
||||
padding-bottom: env(safe-area-inset-bottom);
|
||||
padding-left: env(safe-area-inset-left);
|
||||
transform: translate3d(
|
||||
0,
|
||||
calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height) + env(safe-area-inset-bottom)),
|
||||
0
|
||||
);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height) + env(safe-area-inset-bottom)), 0);
|
||||
|
||||
&.open:not(.in-attachment-modal) {
|
||||
transform: translate3d(0, 0, 0);
|
||||
@ -40,11 +36,7 @@
|
||||
|
||||
&.open.in-attachment-modal {
|
||||
z-index: calc(var(--z-modal) + 1);
|
||||
transform: translate3d(
|
||||
0,
|
||||
calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)),
|
||||
0
|
||||
);
|
||||
transform: translate3d(0, calc(var(--symbol-menu-height) + var(--symbol-menu-footer-height)), 0);
|
||||
}
|
||||
|
||||
// Target: Old Firefox (Waterfox Classic)
|
||||
@ -157,6 +149,12 @@
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
|
||||
&:not(.open) {
|
||||
transform: scale(0.85) !important;
|
||||
}
|
||||
|
||||
transition-duration: 200ms !important;
|
||||
|
||||
@supports (overflow: overlay) {
|
||||
width: var(--symbol-menu-width);
|
||||
}
|
||||
@ -244,6 +242,7 @@
|
||||
&:focus {
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
}
|
||||
|
||||
@media (hover: hover) {
|
||||
&:hover {
|
||||
background-color: var(--color-interactive-element-hover);
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
@ -8,7 +9,6 @@ import type { ApiSticker, ApiVideo } from '../../../api/types';
|
||||
import type { GlobalActions } from '../../../global';
|
||||
|
||||
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import { fastRaf } from '../../../util/schedulers';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { selectTabState, selectIsCurrentUserPremium } from '../../../global/selectors';
|
||||
|
||||
@ -47,7 +47,7 @@ export type OwnProps = {
|
||||
isSilent?: boolean,
|
||||
shouldSchedule?: boolean,
|
||||
shouldPreserveInput?: boolean,
|
||||
shouldUpdateStickerSetsOrder?: boolean
|
||||
shouldUpdateStickerSetsOrder?: boolean,
|
||||
) => void;
|
||||
onGifSelect?: (gif: ApiVideo, isSilent?: boolean, shouldSchedule?: boolean) => void;
|
||||
onRemoveSymbol: () => void;
|
||||
@ -130,24 +130,21 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
}, [isCurrentUserPremium, lastSyncTime, loadPremiumSetStickers]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (!isMobile || isAttachmentModal) {
|
||||
if (!isMobile || !isOpen || isAttachmentModal) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
document.body.classList.add('enable-symbol-menu-transforms');
|
||||
document.body.classList.add('is-symbol-menu-open');
|
||||
}
|
||||
document.body.classList.add('enable-symbol-menu-transforms');
|
||||
document.body.classList.add('is-symbol-menu-open');
|
||||
|
||||
return () => {
|
||||
if (isOpen) {
|
||||
fastRaf(() => {
|
||||
document.body.classList.remove('is-symbol-menu-open');
|
||||
setTimeout(() => {
|
||||
document.body.classList.remove('enable-symbol-menu-transforms');
|
||||
}, ANIMATION_DURATION);
|
||||
document.body.classList.remove('is-symbol-menu-open');
|
||||
|
||||
setTimeout(() => {
|
||||
requestMutation(() => {
|
||||
document.body.classList.remove('enable-symbol-menu-transforms');
|
||||
});
|
||||
}
|
||||
}, ANIMATION_DURATION);
|
||||
};
|
||||
}, [isAttachmentModal, isMobile, isOpen]);
|
||||
|
||||
@ -219,6 +216,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<CustomEmojiPicker
|
||||
className="picker-tab"
|
||||
isHidden={!isOpen || !isActive}
|
||||
loadAndPlay={isOpen && (isActive || isFrom)}
|
||||
onCustomEmojiSelect={handleCustomEmojiSelect}
|
||||
chatId={chatId}
|
||||
@ -228,6 +226,7 @@ const SymbolMenu: FC<OwnProps & StateProps> = ({
|
||||
return (
|
||||
<StickerPicker
|
||||
className="picker-tab"
|
||||
isHidden={!isOpen || !isActive}
|
||||
loadAndPlay={canSendStickers ? isOpen && (isActive || isFrom) : false}
|
||||
canSendStickers={canSendStickers}
|
||||
onStickerSelect={handleStickerSelect}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useCallback, useEffect } from '../../../../lib/teact/teact';
|
||||
import twemojiRegex from '../../../../lib/twemojiRegex';
|
||||
import { requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ApiSticker } from '../../../../api/types';
|
||||
import type { Signal } from '../../../../util/signals';
|
||||
@ -85,7 +86,7 @@ export default function useCustomEmojiTooltip(
|
||||
|
||||
setHtml(`${newHtml}${htmlAfterSelection}`);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestNextMutation(() => {
|
||||
focusEditableElement(inputEl, true, true);
|
||||
});
|
||||
}, [getLastEmoji, isEnabled, inputRef, setHtml]);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect } from '../../../../lib/teact/teact';
|
||||
import { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import type { ApiDraft } from '../../../../global/types';
|
||||
@ -21,7 +22,8 @@ let isFrozen = false;
|
||||
|
||||
function freeze() {
|
||||
isFrozen = true;
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
requestMeasure(() => {
|
||||
isFrozen = false;
|
||||
});
|
||||
}
|
||||
@ -91,7 +93,7 @@ const useDraft = (
|
||||
if (customEmojiIds.length) loadCustomEmojis({ ids: customEmojiIds });
|
||||
|
||||
if (!IS_TOUCH_ENV) {
|
||||
requestAnimationFrame(() => {
|
||||
requestNextMutation(() => {
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
if (messageInput) {
|
||||
focusEditableElement(messageInput, true);
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { useCallback, useEffect, useState } from '../../../../lib/teact/teact';
|
||||
import { getActions } from '../../../../global';
|
||||
import { requestMeasure, requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ApiFormattedText, ApiMessage } from '../../../../api/types';
|
||||
import type { MessageListType } from '../../../../global/types';
|
||||
@ -12,7 +13,6 @@ import parseMessageInput from '../../../../util/parseMessageInput';
|
||||
import focusEditableElement from '../../../../util/focusEditableElement';
|
||||
import { hasMessageMedia } from '../../../../global/helpers';
|
||||
import { getTextWithEntitiesAsHtml } from '../../../common/helpers/renderTextWithEntities';
|
||||
import { fastRaf } from '../../../../util/schedulers';
|
||||
import useBackgroundMode from '../../../../hooks/useBackgroundMode';
|
||||
import useBeforeUnload from '../../../../hooks/useBeforeUnload';
|
||||
import { useDebouncedResolver } from '../../../../hooks/useAsyncResolvers';
|
||||
@ -57,8 +57,8 @@ const useEditing = (
|
||||
|
||||
setHtml(html);
|
||||
setShouldForceShowEditing(true);
|
||||
// `fastRaf` would execute syncronously in this case
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
requestNextMutation(() => {
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
if (messageInput) {
|
||||
focusEditableElement(messageInput, true);
|
||||
@ -72,7 +72,7 @@ const useEditing = (
|
||||
}
|
||||
|
||||
const shouldSetNoWebPage = !('webPage' in editedMessage.content)
|
||||
&& editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type));
|
||||
&& editedMessage.content.text?.entities?.some((entity) => URL_ENTITIES.has(entity.type));
|
||||
|
||||
toggleMessageWebPage({
|
||||
chatId,
|
||||
@ -120,15 +120,18 @@ const useEditing = (
|
||||
|
||||
const restoreNewDraftAfterEditing = useCallback(() => {
|
||||
if (!draft) return;
|
||||
// Run 1 frame after editing draft reset
|
||||
fastRaf(() => {
|
||||
|
||||
// Run one frame after editing draft reset
|
||||
requestMeasure(() => {
|
||||
setHtml(getTextWithEntitiesAsHtml(draft));
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
if (messageInput) {
|
||||
requestAnimationFrame(() => {
|
||||
|
||||
// Wait one more frame until new HTML is rendered
|
||||
requestNextMutation(() => {
|
||||
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
|
||||
if (messageInput) {
|
||||
focusEditableElement(messageInput, true);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}, [draft, setHtml]);
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect, useState } from '../../../../lib/teact/teact';
|
||||
import { requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import { getGlobal } from '../../../../global';
|
||||
|
||||
import type { ApiSticker } from '../../../../api/types';
|
||||
@ -123,7 +124,7 @@ export default function useEmojiTooltip(
|
||||
? document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR)!
|
||||
: document.getElementById(inputId) as HTMLDivElement;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestNextMutation(() => {
|
||||
focusEditableElement(messageInput, true, true);
|
||||
});
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import {
|
||||
useCallback, useEffect, useRef,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import RLottie from '../../../../lib/rlottie/RLottie';
|
||||
import { requestMeasure } from '../../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ApiSticker } from '../../../../api/types';
|
||||
import type { Signal } from '../../../../util/signals';
|
||||
@ -14,7 +15,6 @@ import {
|
||||
removeCustomEmojiInputRenderCallback,
|
||||
} from '../../../../util/customEmojiManager';
|
||||
import { round } from '../../../../util/math';
|
||||
import { fastRaf } from '../../../../util/schedulers';
|
||||
import AbsoluteVideo from '../../../../util/AbsoluteVideo';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
|
||||
@ -131,7 +131,7 @@ export default function useInputCustomEmojis(
|
||||
}
|
||||
|
||||
// Wait one frame for DOM to update
|
||||
fastRaf(() => {
|
||||
requestMeasure(() => {
|
||||
synchronizeElements();
|
||||
});
|
||||
}, [getHtml, synchronizeElements, inputRef, clearPlayers, sharedCanvasRef, isActive]);
|
||||
@ -163,7 +163,7 @@ export default function useInputCustomEmojis(
|
||||
}, []);
|
||||
|
||||
const unfreezeAnimationOnRaf = useCallback(() => {
|
||||
fastRaf(unfreezeAnimation);
|
||||
requestMeasure(unfreezeAnimation);
|
||||
}, [unfreezeAnimation]);
|
||||
|
||||
// Pausing frame may not happen in background,
|
||||
|
||||
@ -3,6 +3,7 @@ import {
|
||||
useCallback, useEffect, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { getGlobal } from '../../../../global';
|
||||
import { requestNextMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ApiChatMember, ApiUser } from '../../../../api/types';
|
||||
import type { Signal } from '../../../../util/signals';
|
||||
@ -120,7 +121,7 @@ export default function useMentionTooltip(
|
||||
const caretPosition = getCaretPosition(inputEl);
|
||||
setHtml(`${newHtml}${htmlAfterSelection}`);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestNextMutation(() => {
|
||||
const newCaretPosition = caretPosition + shiftCaretPosition + 1;
|
||||
focusEditableElement(inputEl, forceFocus);
|
||||
if (newCaretPosition >= 0) {
|
||||
|
||||
@ -1,12 +1,15 @@
|
||||
import {
|
||||
useCallback, useEffect, useRef, useState,
|
||||
} from '../../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { IS_SAFARI, IS_VOICE_RECORDING_SUPPORTED } from '../../../../util/windowEnvironment';
|
||||
import * as voiceRecording from '../../../../util/voiceRecording';
|
||||
import captureEscKeyListener from '../../../../util/captureEscKeyListener';
|
||||
|
||||
type ActiveVoiceRecording = { stop: () => Promise<voiceRecording.Result>; pause: NoneToVoidFunction } | undefined;
|
||||
type ActiveVoiceRecording =
|
||||
{ stop: () => Promise<voiceRecording.Result>; pause: NoneToVoidFunction }
|
||||
| undefined;
|
||||
|
||||
const useVoiceRecording = () => {
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
@ -27,7 +30,9 @@ const useVoiceRecording = () => {
|
||||
const { stop, pause } = await voiceRecording.start((tickVolume: number) => {
|
||||
if (recordButtonRef.current) {
|
||||
if (startRecordTimeRef.current && Date.now() % 4 === 0) {
|
||||
recordButtonRef.current.style.boxShadow = `0 0 0 ${(tickVolume || 0) * 50}px rgba(0,0,0,.15)`;
|
||||
requestMutation(() => {
|
||||
recordButtonRef.current!.style.boxShadow = `0 0 0 ${(tickVolume || 0) * 50}px rgba(0,0,0,.15)`;
|
||||
});
|
||||
}
|
||||
setCurrentRecordTime(Date.now());
|
||||
}
|
||||
@ -47,9 +52,12 @@ const useVoiceRecording = () => {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (recordButtonRef.current) {
|
||||
recordButtonRef.current.style.boxShadow = 'none';
|
||||
}
|
||||
requestMutation(() => {
|
||||
if (recordButtonRef.current) {
|
||||
recordButtonRef.current!.style.boxShadow = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return activeVoiceRecording!.pause();
|
||||
} catch (err) {
|
||||
@ -67,9 +75,13 @@ const useVoiceRecording = () => {
|
||||
setActiveVoiceRecording(undefined);
|
||||
startRecordTimeRef.current = undefined;
|
||||
setCurrentRecordTime(undefined);
|
||||
if (recordButtonRef.current) {
|
||||
recordButtonRef.current.style.boxShadow = 'none';
|
||||
}
|
||||
|
||||
requestMutation(() => {
|
||||
if (recordButtonRef.current) {
|
||||
recordButtonRef.current!.style.boxShadow = 'none';
|
||||
}
|
||||
});
|
||||
|
||||
try {
|
||||
return activeVoiceRecording!.stop();
|
||||
} catch (err) {
|
||||
|
||||
21
src/components/middle/hooks/useAuthorWidth.ts
Normal file
21
src/components/middle/hooks/useAuthorWidth.ts
Normal file
@ -0,0 +1,21 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useLayoutEffect } from '../../../lib/teact/teact';
|
||||
import { requestForcedReflow } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
export default function useAuthorWidth(
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
signature?: string,
|
||||
) {
|
||||
useLayoutEffect(() => {
|
||||
if (!signature) return;
|
||||
|
||||
requestForcedReflow(() => {
|
||||
const width = containerRef.current!.querySelector<HTMLDivElement>('.message-signature')?.offsetWidth;
|
||||
if (!width) return undefined;
|
||||
|
||||
return () => {
|
||||
containerRef.current!.style.setProperty('--meta-safe-author-width', `${width}px`);
|
||||
};
|
||||
});
|
||||
}, [containerRef, signature]);
|
||||
}
|
||||
@ -1,24 +1,33 @@
|
||||
import type { RefObject } from 'react';
|
||||
import {
|
||||
useCallback, useEffect, useMemo, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions } from '../../../global';
|
||||
import { useMemo, useRef } from '../../../lib/teact/teact';
|
||||
|
||||
import { LoadMoreDirection } from '../../../types';
|
||||
import type { MessageListType } from '../../../global/types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
|
||||
import { LOCAL_MESSAGE_MIN_ID } from '../../../config';
|
||||
import { MESSAGE_LIST_SENSITIVE_AREA } from '../../../util/windowEnvironment';
|
||||
import { debounce } from '../../../util/schedulers';
|
||||
import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
import { useStateRef } from '../../../hooks/useStateRef';
|
||||
import { useSignalEffect } from '../../../hooks/useSignalEffect';
|
||||
import { useDebouncedSignal } from '../../../hooks/useAsyncResolvers';
|
||||
|
||||
const FAB_THRESHOLD = 50;
|
||||
const NOTCH_THRESHOLD = 1; // Notch has zero height so we at least need a 1px margin to intersect
|
||||
const CONTAINER_HEIGHT_DEBOUNCE = 100;
|
||||
const TOOLS_FREEZE_TIMEOUT = 250; // Approximate message sending animation duration
|
||||
|
||||
export default function useScrollHooks(
|
||||
type: MessageListType,
|
||||
containerRef: RefObject<HTMLDivElement>,
|
||||
messageIds: number[],
|
||||
getContainerHeight: Signal<number | undefined>,
|
||||
isViewportNewest: boolean,
|
||||
isUnread: boolean,
|
||||
onFabToggle: AnyToVoidFunction,
|
||||
@ -58,11 +67,12 @@ export default function useScrollHooks(
|
||||
return;
|
||||
}
|
||||
|
||||
if (!containerRef.current) {
|
||||
const container = containerRef.current;
|
||||
if (!container) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { offsetHeight, scrollHeight, scrollTop } = containerRef.current;
|
||||
const { offsetHeight, scrollHeight, scrollTop } = container;
|
||||
const scrollBottom = Math.round(scrollHeight - scrollTop - offsetHeight);
|
||||
const isNearBottom = scrollBottom <= FAB_THRESHOLD;
|
||||
const isAtBottom = scrollBottom <= NOTCH_THRESHOLD;
|
||||
@ -113,6 +123,7 @@ export default function useScrollHooks(
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
margin: FAB_THRESHOLD * 2,
|
||||
throttleScheduler: requestMeasure,
|
||||
}, toggleScrollTools);
|
||||
|
||||
useOnIntersect(fabTriggerRef, observeIntersectionForFab);
|
||||
@ -124,20 +135,19 @@ export default function useScrollHooks(
|
||||
} = useIntersectionObserver({
|
||||
rootRef: containerRef,
|
||||
margin: NOTCH_THRESHOLD,
|
||||
throttleScheduler: requestMeasure,
|
||||
}, toggleScrollTools);
|
||||
|
||||
useOnIntersect(fabTriggerRef, observeIntersectionForNotch);
|
||||
|
||||
const toggleScrollToolsRef = useRef<typeof toggleScrollTools>();
|
||||
toggleScrollToolsRef.current = toggleScrollTools;
|
||||
useSyncEffect(() => {
|
||||
const toggleScrollToolsRef = useStateRef(toggleScrollTools);
|
||||
useEffect(() => {
|
||||
if (isReady) {
|
||||
toggleScrollToolsRef.current!();
|
||||
}
|
||||
}, [isReady]);
|
||||
}, [isReady, toggleScrollToolsRef]);
|
||||
|
||||
// Workaround for FAB and notch flickering with tall incoming message
|
||||
useSyncEffect(() => {
|
||||
const freezeShortly = useCallback(() => {
|
||||
freezeForFab();
|
||||
freezeForNotch();
|
||||
|
||||
@ -145,7 +155,14 @@ export default function useScrollHooks(
|
||||
unfreezeForNotch();
|
||||
unfreezeForFab();
|
||||
}, TOOLS_FREEZE_TIMEOUT);
|
||||
}, [freezeForFab, freezeForNotch, messageIds, unfreezeForFab, unfreezeForNotch]);
|
||||
}, [freezeForFab, freezeForNotch, unfreezeForFab, unfreezeForNotch]);
|
||||
|
||||
// Workaround for FAB and notch flickering with tall incoming message
|
||||
useSyncEffect(freezeShortly, [freezeShortly, messageIds]);
|
||||
|
||||
// Workaround for notch flickering when opening Composer Embedded Message
|
||||
const getContainerHeightDebounced = useDebouncedSignal(getContainerHeight, CONTAINER_HEIGHT_DEBOUNCE);
|
||||
useSignalEffect(freezeShortly, [freezeShortly, getContainerHeightDebounced]);
|
||||
|
||||
return { backwardsTriggerRef, forwardsTriggerRef, fabTriggerRef };
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useCallback } from '../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { fastRaf } from '../../../util/schedulers';
|
||||
import useRunDebounced from '../../../hooks/useRunDebounced';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
|
||||
@ -19,23 +19,25 @@ export default function useStickyDates() {
|
||||
markIsScrolled();
|
||||
|
||||
if (!document.body.classList.contains('is-scrolling-messages')) {
|
||||
fastRaf(() => {
|
||||
requestMutation(() => {
|
||||
document.body.classList.add('is-scrolling-messages');
|
||||
});
|
||||
}
|
||||
|
||||
runDebounced(() => {
|
||||
fastRaf(() => {
|
||||
const stuckDateEl = findStuckDate(container, hasTools);
|
||||
if (stuckDateEl) {
|
||||
requestMutation(() => {
|
||||
stuckDateEl.classList.add('stuck');
|
||||
});
|
||||
}
|
||||
|
||||
requestMutation(() => {
|
||||
const currentStuck = document.querySelector('.stuck');
|
||||
if (currentStuck) {
|
||||
currentStuck.classList.remove('stuck');
|
||||
}
|
||||
|
||||
const stuckDateEl = findStuckDate(container, hasTools);
|
||||
if (stuckDateEl) {
|
||||
stuckDateEl.classList.add('stuck');
|
||||
}
|
||||
|
||||
document.body.classList.remove('is-scrolling-messages');
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
@ -34,9 +35,10 @@ import useLayoutEffectWithPrevDeps from '../../../hooks/useLayoutEffectWithPrevD
|
||||
import Avatar from '../../common/Avatar';
|
||||
import Skeleton from '../../ui/Skeleton';
|
||||
|
||||
import mapPin from '../../../assets/map-pin.svg';
|
||||
import './Location.scss';
|
||||
|
||||
import mapPin from '../../../assets/map-pin.svg';
|
||||
|
||||
const MOVE_THRESHOLD = 0.0001; // ~11m
|
||||
const DEFAULT_MAP_CONFIG = {
|
||||
width: 400,
|
||||
@ -156,9 +158,11 @@ const Location: FC<OwnProps> = ({
|
||||
if (mapBlobUrl) {
|
||||
const contentEl = ref.current!.closest<HTMLDivElement>(MESSAGE_CONTENT_SELECTOR)!;
|
||||
getCustomAppendixBg(mapBlobUrl, isOwn, isInSelectMode, isSelected, theme).then((appendixBg) => {
|
||||
contentEl.style.setProperty('--appendix-bg', appendixBg);
|
||||
contentEl.classList.add('has-appendix-thumb');
|
||||
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
|
||||
requestMutation(() => {
|
||||
contentEl.style.setProperty('--appendix-bg', appendixBg);
|
||||
contentEl.classList.add('has-appendix-thumb');
|
||||
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
|
||||
});
|
||||
});
|
||||
}
|
||||
}, [shouldRenderText, isOwn, isInSelectMode, isSelected, theme, mapBlobUrl]);
|
||||
@ -182,11 +186,12 @@ const Location: FC<OwnProps> = ({
|
||||
}, !isExpired ? (secondsBeforeEnd || 0) * 1000 : undefined);
|
||||
|
||||
useInterval(() => {
|
||||
const countdownEl = countdownRef.current;
|
||||
|
||||
if (countdownEl) {
|
||||
updateCountdown(countdownEl);
|
||||
}
|
||||
requestMutation(() => {
|
||||
const countdownEl = countdownRef.current;
|
||||
if (countdownEl) {
|
||||
updateCountdown(countdownEl);
|
||||
}
|
||||
});
|
||||
}, secondsBeforeEnd ? 1000 : undefined);
|
||||
|
||||
function renderInfo() {
|
||||
|
||||
@ -106,7 +106,6 @@ import {
|
||||
} from './helpers/mediaDimensions';
|
||||
import { calculateAlbumLayout } from './helpers/calculateAlbumLayout';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
import calculateAuthorWidth from './helpers/calculateAuthorWidth';
|
||||
import { getServerTime } from '../../../util/serverTime';
|
||||
import { isElementInViewport } from '../../../util/isElementInViewport';
|
||||
import { getCustomEmojiSize } from '../composer/helpers/customEmoji';
|
||||
@ -127,6 +126,7 @@ import useThrottledCallback from '../../../hooks/useThrottledCallback';
|
||||
import useMessageTranslation from './hooks/useMessageTranslation';
|
||||
import usePrevious from '../../../hooks/usePrevious';
|
||||
import useTextLanguage from '../../../hooks/useTextLanguage';
|
||||
import useAuthorWidth from '../hooks/useAuthorWidth';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Avatar from '../../common/Avatar';
|
||||
@ -388,9 +388,12 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
useOnIntersect(bottomMarkerRef, observeIntersectionForBottom);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition,
|
||||
handleBeforeContextMenu, handleContextMenu: onContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
isContextMenuOpen,
|
||||
contextMenuPosition,
|
||||
handleBeforeContextMenu,
|
||||
handleContextMenu: onContextMenu,
|
||||
handleContextMenuClose,
|
||||
handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, IS_TOUCH_ENV && isInSelectMode, true, IS_ANDROID);
|
||||
|
||||
useEffect(() => {
|
||||
@ -560,7 +563,7 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
Boolean(message.views) && 'has-views',
|
||||
message.isEdited && 'was-edited',
|
||||
hasReply && 'has-reply',
|
||||
isContextMenuShown && 'has-menu-open',
|
||||
isContextMenuOpen && 'has-menu-open',
|
||||
isFocused && !noFocusHighlight && 'focused',
|
||||
isForwarding && 'is-forwarding',
|
||||
message.isDeleting && 'is-deleting',
|
||||
@ -658,13 +661,19 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
ref, messageId, chatId, isFocused, focusDirection, noFocusHighlight, viewportIds, isResizingContainer,
|
||||
);
|
||||
|
||||
const signature = (isChannel && message.postAuthorTitle)
|
||||
|| (!asForwarded && forwardInfo?.postAuthorTitle)
|
||||
|| undefined;
|
||||
useAuthorWidth(ref, signature);
|
||||
|
||||
const shouldFocusOnResize = isLastInGroup;
|
||||
|
||||
const handleResize = useCallback((entry: ResizeObserverEntry) => {
|
||||
const lastHeight = messageHeightRef.current;
|
||||
|
||||
const newHeight = entry.target.clientHeight;
|
||||
const newHeight = entry.contentRect.height;
|
||||
messageHeightRef.current = newHeight;
|
||||
|
||||
if (isAnimatingScroll() || !lastHeight || newHeight <= lastHeight) return;
|
||||
|
||||
const container = entry.target.closest<HTMLDivElement>('.MessageList');
|
||||
@ -754,13 +763,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
reactionsMaxWidth = width + EXTRA_SPACE_FOR_REACTIONS;
|
||||
}
|
||||
|
||||
const signature = (isChannel && message.postAuthorTitle)
|
||||
|| (!asForwarded && forwardInfo?.postAuthorTitle)
|
||||
|| undefined;
|
||||
const metaSafeAuthorWidth = useMemo(() => {
|
||||
return signature ? calculateAuthorWidth(signature) : undefined;
|
||||
}, [signature]);
|
||||
|
||||
function renderAvatar() {
|
||||
const isAvatarPeerUser = avatarPeer && isUserId(avatarPeer.id);
|
||||
const avatarUser = (avatarPeer && isAvatarPeerUser) ? avatarPeer as ApiUser : undefined;
|
||||
@ -1208,7 +1210,6 @@ const Message: FC<OwnProps & StateProps> = ({
|
||||
ref={ref}
|
||||
id={getMessageHtmlId(message.id)}
|
||||
className={containerClassName}
|
||||
style={metaSafeAuthorWidth ? `--meta-safe-author-width: ${metaSafeAuthorWidth}px` : undefined}
|
||||
data-message-id={messageId}
|
||||
onMouseDown={handleMouseDown}
|
||||
onClick={handleClick}
|
||||
|
||||
@ -14,8 +14,6 @@
|
||||
}
|
||||
|
||||
.bubble {
|
||||
transition: opacity 0.15s cubic-bezier(0.2, 0, 0.2, 1), transform 0.15s cubic-bezier(0.2, 0, 0.2, 1) !important;
|
||||
transform: scale(0.7);
|
||||
overflow: initial;
|
||||
padding: 0 !important;
|
||||
}
|
||||
@ -35,7 +33,6 @@
|
||||
padding: 0.25rem 0;
|
||||
}
|
||||
|
||||
|
||||
.backdrop {
|
||||
touch-action: none;
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import React, {
|
||||
useCallback, useRef, useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
@ -159,8 +160,10 @@ const Photo: FC<OwnProps> = ({
|
||||
const contentEl = ref.current!.closest<HTMLDivElement>(MESSAGE_CONTENT_SELECTOR)!;
|
||||
if (fullMediaData) {
|
||||
getCustomAppendixBg(fullMediaData, isOwn, isInSelectMode, isSelected, theme).then((appendixBg) => {
|
||||
contentEl.style.setProperty('--appendix-bg', appendixBg);
|
||||
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
|
||||
requestMutation(() => {
|
||||
contentEl.style.setProperty('--appendix-bg', appendixBg);
|
||||
contentEl.setAttribute(CUSTOM_APPENDIX_ATTRIBUTE, '');
|
||||
});
|
||||
});
|
||||
} else {
|
||||
contentEl.classList.add('has-appendix-thumb');
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { useState, useEffect, useRef } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
useState, useEffect, useRef, useLayoutEffect,
|
||||
} from '../../../lib/teact/teact';
|
||||
|
||||
import type { ApiPollAnswer, ApiPollResult } from '../../../api/types';
|
||||
|
||||
@ -41,7 +43,7 @@ const PollOption: FC<OwnProps> = ({
|
||||
}
|
||||
}, [shouldAnimate, answerPercent]);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
const lineEl = lineRef.current;
|
||||
|
||||
if (lineEl && shouldAnimate) {
|
||||
|
||||
@ -1,23 +1,23 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback,
|
||||
useEffect,
|
||||
useLayoutEffect,
|
||||
useRef,
|
||||
useState,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import { getActions } from '../../../global';
|
||||
|
||||
import type { ApiMessage } from '../../../api/types';
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { ApiMediaFormat } from '../../../api/types';
|
||||
import { ROUND_VIDEO_DIMENSIONS_PX } from '../../common/helpers/mediaDimensions';
|
||||
import { getMessageMediaFormat, getMessageMediaHash, getMessageMediaThumbDataUri } from '../../../global/helpers';
|
||||
import { formatMediaDuration } from '../../../util/dateFormat';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import { stopCurrentAudio } from '../../../util/audioPlayer';
|
||||
import safePlay from '../../../util/safePlay';
|
||||
import { fastRaf } from '../../../util/schedulers';
|
||||
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
||||
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
|
||||
import useMediaWithLoadProgress from '../../../hooks/useMediaWithLoadProgress';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
@ -92,7 +92,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
const [isActivated, setIsActivated] = useState<boolean>(false);
|
||||
const [progress, setProgress] = useState<number>(0);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
if (!isActivated) {
|
||||
return;
|
||||
}
|
||||
@ -133,7 +133,7 @@ const RoundVideo: FC<OwnProps> = ({
|
||||
setProgress(0);
|
||||
safePlay(playerRef.current);
|
||||
|
||||
fastRaf(() => {
|
||||
requestMutation(() => {
|
||||
playingProgressRef.current!.innerHTML = '';
|
||||
});
|
||||
}, []);
|
||||
|
||||
@ -1,21 +0,0 @@
|
||||
let element: HTMLSpanElement | undefined;
|
||||
let fontFamily: string | undefined;
|
||||
export default function calculateAuthorWidth(text: string) {
|
||||
if (!fontFamily) {
|
||||
fontFamily = getComputedStyle(document.documentElement).getPropertyValue('--font-family');
|
||||
}
|
||||
|
||||
if (!element) {
|
||||
element = document.createElement('span');
|
||||
element.style.font = `400 12px ${fontFamily}`;
|
||||
element.style.whiteSpace = 'nowrap';
|
||||
element.style.position = 'absolute';
|
||||
element.style.left = '-999px';
|
||||
element.style.opacity = '.01';
|
||||
document.body.appendChild(element);
|
||||
}
|
||||
|
||||
element.textContent = text;
|
||||
|
||||
return element.offsetWidth;
|
||||
}
|
||||
@ -1,6 +1,7 @@
|
||||
import { useLayoutEffect, useMemo } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { FocusDirection } from '../../../../types';
|
||||
|
||||
import { useLayoutEffect, useMemo } from '../../../../lib/teact/teact';
|
||||
import fastSmoothScroll from '../../../../util/fastSmoothScroll';
|
||||
|
||||
// This is used when the viewport was replaced.
|
||||
@ -42,6 +43,8 @@ export default function useFocusMessage(
|
||||
focusDirection,
|
||||
undefined,
|
||||
isResizingContainer,
|
||||
// We need this to override scroll setting from Message List layout effect
|
||||
true,
|
||||
);
|
||||
}
|
||||
}, [
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type React from '../../../../lib/teact/teact';
|
||||
import { useEffect, useRef } from '../../../../lib/teact/teact';
|
||||
import { requestMeasure } from '../../../../lib/fasterdom/fasterdom';
|
||||
import { getActions } from '../../../../global';
|
||||
|
||||
import { IS_ANDROID, IS_TOUCH_ENV } from '../../../../util/windowEnvironment';
|
||||
@ -10,6 +11,7 @@ import useFlag from '../../../../hooks/useFlag';
|
||||
import { preventMessageInputBlur } from '../../helpers/preventMessageInputBlur';
|
||||
import stopEvent from '../../../../util/stopEvent';
|
||||
import { REM } from '../../../common/helpers/mediaDimensions';
|
||||
import useThrottledCallback from '../../../../hooks/useThrottledCallback';
|
||||
|
||||
const ANDROID_KEYBOARD_HIDE_DELAY_MS = 350;
|
||||
const SWIPE_ANIMATION_DURATION = 150;
|
||||
@ -45,7 +47,7 @@ export default function useOuterHandlers(
|
||||
handleBeforeContextMenu(e);
|
||||
}
|
||||
|
||||
function handleMouseMove(e: React.MouseEvent) {
|
||||
const handleMouseMove = useThrottledCallback((e: React.MouseEvent) => {
|
||||
const quickReactionContainer = quickReactionRef.current;
|
||||
if (!quickReactionContainer) return;
|
||||
|
||||
@ -63,7 +65,7 @@ export default function useOuterHandlers(
|
||||
} else {
|
||||
unmarkQuickReactionVisible();
|
||||
}
|
||||
}
|
||||
}, [quickReactionRef], requestMeasure);
|
||||
|
||||
function handleSendQuickReaction(e: React.MouseEvent<HTMLDivElement, MouseEvent>) {
|
||||
e.stopPropagation();
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import { useCallback, useEffect, useRef } from '../../../../lib/teact/teact';
|
||||
import { requestMeasure } from '../../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { fastRaf } from '../../../../util/schedulers';
|
||||
import useBackgroundMode, { isBackgroundModeActive } from '../../../../hooks/useBackgroundMode';
|
||||
import useHeavyAnimationCheck, { isHeavyAnimating } from '../../../../hooks/useHeavyAnimationCheck';
|
||||
import usePriorityPlaybackCheck, { isPriorityPlaybackActive } from '../../../../hooks/usePriorityPlaybackCheck';
|
||||
@ -18,7 +18,7 @@ export default function useVideoAutoPause(playerRef: { current: HTMLVideoElement
|
||||
}, [play]);
|
||||
|
||||
const unfreezePlayingOnRaf = useCallback(() => {
|
||||
fastRaf(unfreezePlaying);
|
||||
requestMeasure(unfreezePlaying);
|
||||
}, [unfreezePlaying]);
|
||||
|
||||
useBackgroundMode(pause, unfreezePlayingOnRaf, !canPlay);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useCallback, useEffect } from '../../../lib/teact/teact';
|
||||
import { requestMutation, requestMeasure } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
export default function useTransitionFixes(
|
||||
containerRef: { current: HTMLDivElement | null },
|
||||
@ -11,7 +12,11 @@ export default function useTransitionFixes(
|
||||
const transitionEl = container.querySelector<HTMLDivElement>(transitionElSelector);
|
||||
const tabsEl = container.querySelector<HTMLDivElement>('.TabList');
|
||||
if (transitionEl && tabsEl) {
|
||||
transitionEl.style.minHeight = `${container.offsetHeight - tabsEl.offsetHeight}px`;
|
||||
const newHeight = container.offsetHeight - tabsEl.offsetHeight;
|
||||
|
||||
requestMutation(() => {
|
||||
transitionEl.style.minHeight = `${newHeight}px`;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -26,12 +31,18 @@ export default function useTransitionFixes(
|
||||
|
||||
// Workaround for scrollable content flickering during animation.
|
||||
const applyTransitionFix = useCallback(() => {
|
||||
const container = containerRef.current!;
|
||||
if (container.style.overflowY !== 'hidden') {
|
||||
// This callback is called from `Transition.onStart` which is "mutate" phase
|
||||
requestMeasure(() => {
|
||||
const container = containerRef.current!;
|
||||
if (container.style.overflowY === 'hidden') return;
|
||||
|
||||
const scrollBarWidth = container.offsetWidth - container.clientWidth;
|
||||
container.style.overflowY = 'hidden';
|
||||
container.style.paddingRight = `${scrollBarWidth}px`;
|
||||
}
|
||||
|
||||
requestMutation(() => {
|
||||
container.style.overflowY = 'hidden';
|
||||
container.style.paddingRight = `${scrollBarWidth}px`;
|
||||
});
|
||||
});
|
||||
}, [containerRef]);
|
||||
|
||||
const releaseTransitionFix = useCallback(() => {
|
||||
|
||||
@ -3,6 +3,7 @@ import type { MouseEvent as ReactMouseEvent, RefObject } from 'react';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useRef, useCallback, useState } from '../../lib/teact/teact';
|
||||
|
||||
import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import buildStyle from '../../util/buildStyle';
|
||||
|
||||
@ -34,6 +35,7 @@ export type OwnProps = {
|
||||
download?: string;
|
||||
disabled?: boolean;
|
||||
allowDisabledClick?: boolean;
|
||||
noFastClick?: boolean;
|
||||
ripple?: boolean;
|
||||
faded?: boolean;
|
||||
tabIndex?: number;
|
||||
@ -85,6 +87,7 @@ const Button: FC<OwnProps> = ({
|
||||
download,
|
||||
disabled,
|
||||
allowDisabledClick,
|
||||
noFastClick = color === 'danger',
|
||||
ripple,
|
||||
faded,
|
||||
tabIndex,
|
||||
@ -140,7 +143,11 @@ const Button: FC<OwnProps> = ({
|
||||
if ((allowDisabledClick || !disabled) && onMouseDown) {
|
||||
onMouseDown(e);
|
||||
}
|
||||
}, [allowDisabledClick, disabled, noPreventDefault, onMouseDown]);
|
||||
|
||||
if (!IS_TOUCH_ENV && e.button === MouseButton.Main && !noFastClick) {
|
||||
handleClick(e);
|
||||
}
|
||||
}, [allowDisabledClick, disabled, handleClick, noFastClick, noPreventDefault, onMouseDown]);
|
||||
|
||||
if (href) {
|
||||
return (
|
||||
@ -172,7 +179,7 @@ const Button: FC<OwnProps> = ({
|
||||
id={id}
|
||||
type={type}
|
||||
className={fullClassName}
|
||||
onClick={handleClick}
|
||||
onClick={IS_TOUCH_ENV || noFastClick ? handleClick : undefined}
|
||||
onContextMenu={onContextMenu}
|
||||
onMouseDown={handleMouseDown}
|
||||
onMouseEnter={onMouseEnter && !disabled ? onMouseEnter : undefined}
|
||||
|
||||
@ -85,16 +85,14 @@ const Draggable: FC<OwnProps> = ({
|
||||
}, [id, onDrag, state.origin.x, state.origin.y]);
|
||||
|
||||
const handleMouseUp = useCallback(() => {
|
||||
requestAnimationFrame(() => {
|
||||
setState((current) => ({
|
||||
...current,
|
||||
isDragging: false,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
}));
|
||||
setState((current) => ({
|
||||
...current,
|
||||
isDragging: false,
|
||||
width: undefined,
|
||||
height: undefined,
|
||||
}));
|
||||
|
||||
onDragEnd();
|
||||
});
|
||||
onDragEnd();
|
||||
}, [onDragEnd]);
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
@ -1,10 +1,11 @@
|
||||
import type { RefObject, UIEvent } from 'react';
|
||||
import { LoadMoreDirection } from '../../types';
|
||||
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useCallback, useEffect, useLayoutEffect, useMemo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestForcedReflow } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { LoadMoreDirection } from '../../types';
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
|
||||
import { debounce } from '../../util/schedulers';
|
||||
import resetScroll from '../../util/resetScroll';
|
||||
@ -108,36 +109,41 @@ const InfiniteScroll: FC<OwnProps> = ({
|
||||
|
||||
// Restore `scrollTop` after adding items
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
const state = stateRef.current;
|
||||
requestForcedReflow(() => {
|
||||
const container = containerRef.current!;
|
||||
const state = stateRef.current;
|
||||
|
||||
state.listItemElements = container.querySelectorAll<HTMLDivElement>(itemSelector);
|
||||
state.listItemElements = container.querySelectorAll<HTMLDivElement>(itemSelector);
|
||||
|
||||
let newScrollTop;
|
||||
let newScrollTop: number;
|
||||
|
||||
if (state.currentAnchor && Array.from(state.listItemElements).includes(state.currentAnchor)) {
|
||||
const { scrollTop } = container;
|
||||
const newAnchorTop = state.currentAnchor.getBoundingClientRect().top;
|
||||
newScrollTop = scrollTop + (newAnchorTop - state.currentAnchorTop!);
|
||||
} else {
|
||||
const nextAnchor = state.listItemElements[0];
|
||||
if (nextAnchor) {
|
||||
state.currentAnchor = nextAnchor;
|
||||
state.currentAnchorTop = nextAnchor.getBoundingClientRect().top;
|
||||
if (state.currentAnchor && Array.from(state.listItemElements).includes(state.currentAnchor)) {
|
||||
const { scrollTop } = container;
|
||||
const newAnchorTop = state.currentAnchor!.getBoundingClientRect().top;
|
||||
newScrollTop = scrollTop + (newAnchorTop - state.currentAnchorTop!);
|
||||
} else {
|
||||
const nextAnchor = state.listItemElements[0];
|
||||
if (nextAnchor) {
|
||||
state.currentAnchor = nextAnchor;
|
||||
state.currentAnchorTop = nextAnchor.getBoundingClientRect().top;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (withAbsolutePositioning || noScrollRestore) {
|
||||
return;
|
||||
}
|
||||
if (withAbsolutePositioning || noScrollRestore) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
if (noScrollRestoreOnTop && container.scrollTop === 0) {
|
||||
return;
|
||||
}
|
||||
const { scrollTop } = container;
|
||||
if (noScrollRestoreOnTop && scrollTop === 0) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
resetScroll(container, newScrollTop);
|
||||
return () => {
|
||||
resetScroll(container, newScrollTop);
|
||||
|
||||
state.isScrollTopJustUpdated = true;
|
||||
state.isScrollTopJustUpdated = true;
|
||||
};
|
||||
});
|
||||
}, [items, itemSelector, noScrollRestore, noScrollRestoreOnTop, cacheBuster, withAbsolutePositioning]);
|
||||
|
||||
const handleScroll = useCallback((e: UIEvent<HTMLDivElement>) => {
|
||||
|
||||
@ -1,9 +1,9 @@
|
||||
import type { RefObject } from 'react';
|
||||
import type { FC, TeactNode } from '../../lib/teact/teact';
|
||||
import React, { useRef, useCallback } from '../../lib/teact/teact';
|
||||
import { requestMeasure } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
|
||||
import { fastRaf } from '../../util/schedulers';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
@ -158,7 +158,7 @@ const ListItem: FC<OwnProps> = ({
|
||||
|
||||
if (IS_TOUCH_ENV && !ripple) {
|
||||
markIsTouched();
|
||||
fastRaf(unmarkIsTouched);
|
||||
requestMeasure(unmarkIsTouched);
|
||||
}
|
||||
}, [allowDisabledClick, clickArg, disabled, markIsTouched, onClick, ripple, unmarkIsTouched, href]);
|
||||
|
||||
|
||||
@ -27,8 +27,8 @@
|
||||
z-index: var(--z-menu-bubble);
|
||||
overscroll-behavior: contain;
|
||||
|
||||
transform: scale(0.5);
|
||||
transition: opacity 0.2s cubic-bezier(0.2, 0, 0.2, 1), transform 0.2s cubic-bezier(0.2, 0, 0.2, 1) !important;
|
||||
transform: scale(0.85);
|
||||
transition: opacity 150ms cubic-bezier(0.2, 0, 0.2, 1), transform 150ms cubic-bezier(0.2, 0, 0.2, 1) !important;
|
||||
|
||||
&.open {
|
||||
transform: scale(1);
|
||||
|
||||
@ -10,7 +10,7 @@ import buildClassName from '../../util/buildClassName';
|
||||
import { enableDirectTextInput, disableDirectTextInput } from '../../util/directInputManager';
|
||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||
import useShowTransition from '../../hooks/useShowTransition';
|
||||
import useEffectWithPrevDeps from '../../hooks/useEffectWithPrevDeps';
|
||||
import useLayoutEffectWithPrevDeps from '../../hooks/useLayoutEffectWithPrevDeps';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useHistoryBack from '../../hooks/useHistoryBack';
|
||||
|
||||
@ -88,7 +88,7 @@ const Modal: FC<OwnProps & StateProps> = ({
|
||||
onBack: onClose,
|
||||
});
|
||||
|
||||
useEffectWithPrevDeps(([prevIsOpen]) => {
|
||||
useLayoutEffectWithPrevDeps(([prevIsOpen]) => {
|
||||
document.body.classList.toggle('has-open-dialog', Boolean(isOpen));
|
||||
|
||||
if (isOpen || (!isOpen && prevIsOpen !== undefined)) {
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useEffect, useRef, memo } from '../../lib/teact/teact';
|
||||
import React, { useRef, memo, useLayoutEffect } from '../../lib/teact/teact';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
|
||||
@ -32,18 +32,15 @@ const ProgressSpinner: FC<{
|
||||
const borderRadius = radius - 1;
|
||||
const circumference = circleRadius * 2 * Math.PI;
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const container = useRef<HTMLDivElement>(null);
|
||||
const containerRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (!container.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
const svg = container.current.firstElementChild;
|
||||
useLayoutEffect(() => {
|
||||
const container = containerRef.current!;
|
||||
const svg = container.firstElementChild;
|
||||
const strokeDashOffset = circumference - Math.min(Math.max(MIN_PROGRESS, progress), MAX_PROGRESS) * circumference;
|
||||
|
||||
if (!svg) {
|
||||
container.current.innerHTML = `<svg
|
||||
container.innerHTML = `<svg
|
||||
viewBox="0 0 ${borderRadius * 2} ${borderRadius * 2}"
|
||||
height="${borderRadius * 2}"
|
||||
width="${borderRadius * 2}"
|
||||
@ -63,7 +60,7 @@ const ProgressSpinner: FC<{
|
||||
} else {
|
||||
(svg.firstElementChild as SVGElement).setAttribute('stroke-dashoffset', strokeDashOffset.toString());
|
||||
}
|
||||
}, [container, circumference, borderRadius, circleRadius, progress]);
|
||||
}, [containerRef, circumference, borderRadius, circleRadius, progress]);
|
||||
|
||||
const className = buildClassName(
|
||||
`ProgressSpinner size-${size}`,
|
||||
@ -74,7 +71,7 @@ const ProgressSpinner: FC<{
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={container}
|
||||
ref={containerRef}
|
||||
className={className}
|
||||
onClick={onClick}
|
||||
/>
|
||||
|
||||
@ -28,9 +28,8 @@ const RippleEffect: FC = () => {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = e.currentTarget as HTMLDivElement;
|
||||
const position = container.getBoundingClientRect() as DOMRect;
|
||||
|
||||
const container = e.currentTarget;
|
||||
const position = container.getBoundingClientRect();
|
||||
const rippleSize = container.offsetWidth / 2;
|
||||
|
||||
setRipples([
|
||||
@ -42,9 +41,7 @@ const RippleEffect: FC = () => {
|
||||
},
|
||||
]);
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
cleanUpDebounced();
|
||||
});
|
||||
cleanUpDebounced();
|
||||
}, [ripples, cleanUpDebounced]);
|
||||
|
||||
return (
|
||||
|
||||
@ -1,8 +1,15 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useRef, memo, useEffect } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
useRef,
|
||||
memo,
|
||||
useEffect,
|
||||
useLayoutEffect, useCallback,
|
||||
} from '../../lib/teact/teact';
|
||||
import { requestForcedReflow, requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { IS_TOUCH_ENV, MouseButton } from '../../util/windowEnvironment';
|
||||
import forceReflow from '../../util/forceReflow';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
|
||||
import './Tab.scss';
|
||||
@ -38,12 +45,14 @@ const Tab: FC<OwnProps> = ({
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const tabRef = useRef<HTMLDivElement>(null);
|
||||
|
||||
useEffect(() => {
|
||||
useLayoutEffect(() => {
|
||||
// Set initial active state
|
||||
if (isActive && previousActiveTab === undefined && tabRef.current) {
|
||||
tabRef.current.classList.add(classNames.active);
|
||||
tabRef.current!.classList.add(classNames.active);
|
||||
}
|
||||
}, [isActive, previousActiveTab]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isActive || previousActiveTab === undefined) {
|
||||
return;
|
||||
}
|
||||
@ -53,7 +62,9 @@ const Tab: FC<OwnProps> = ({
|
||||
if (!prevTabEl) {
|
||||
// The number of tabs in the parent component has decreased. It is necessary to add the active tab class name.
|
||||
if (isActive && !tabEl.classList.contains(classNames.active)) {
|
||||
tabEl.classList.add(classNames.active);
|
||||
requestMutation(() => {
|
||||
tabEl.classList.add(classNames.active);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
@ -65,21 +76,38 @@ const Tab: FC<OwnProps> = ({
|
||||
const shiftLeft = prevPlatformEl.parentElement!.offsetLeft - platformEl.parentElement!.offsetLeft;
|
||||
const scaleFactor = prevPlatformEl.clientWidth / platformEl.clientWidth;
|
||||
|
||||
prevPlatformEl.classList.remove('animate');
|
||||
platformEl.classList.remove('animate');
|
||||
platformEl.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
|
||||
forceReflow(platformEl);
|
||||
platformEl.classList.add('animate');
|
||||
platformEl.style.transform = 'none';
|
||||
requestMutation(() => {
|
||||
prevPlatformEl.classList.remove('animate');
|
||||
platformEl.classList.remove('animate');
|
||||
platformEl.style.transform = `translate3d(${shiftLeft}px, 0, 0) scale3d(${scaleFactor}, 1, 1)`;
|
||||
|
||||
prevTabEl.classList.remove(classNames.active);
|
||||
tabEl.classList.add(classNames.active);
|
||||
requestForcedReflow(() => {
|
||||
forceReflow(platformEl);
|
||||
|
||||
return () => {
|
||||
platformEl.classList.add('animate');
|
||||
platformEl.style.transform = 'none';
|
||||
|
||||
prevTabEl.classList.remove(classNames.active);
|
||||
tabEl.classList.add(classNames.active);
|
||||
};
|
||||
});
|
||||
});
|
||||
}, [isActive, previousActiveTab]);
|
||||
|
||||
const handleClick = useCallback((e: React.MouseEvent<HTMLDivElement>) => {
|
||||
if (e.type === 'mousedown' && e.button !== MouseButton.Main) {
|
||||
return;
|
||||
}
|
||||
|
||||
onClick(clickArg);
|
||||
}, [clickArg, onClick]);
|
||||
|
||||
return (
|
||||
<div
|
||||
className={buildClassName('Tab', className)}
|
||||
onClick={() => onClick(clickArg)}
|
||||
onClick={IS_TOUCH_ENV ? handleClick : undefined}
|
||||
onMouseDown={!IS_TOUCH_ENV ? handleClick : undefined}
|
||||
ref={tabRef}
|
||||
>
|
||||
<span>
|
||||
|
||||
@ -1,7 +1,10 @@
|
||||
import type { RefObject } from 'react';
|
||||
import React, { useLayoutEffect, useRef } from '../../lib/teact/teact';
|
||||
import React, { useEffect, useLayoutEffect, useRef } from '../../lib/teact/teact';
|
||||
import { addExtraClass, removeExtraClass, toggleExtraClass } from '../../lib/teact/teact-dom';
|
||||
import { requestMutation, requestForcedReflow } from '../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { getGlobal } from '../../global';
|
||||
|
||||
import type { GlobalState } from '../../global/types';
|
||||
|
||||
import { ANIMATION_LEVEL_MIN } from '../../config';
|
||||
@ -169,9 +172,6 @@ function Transition({
|
||||
return;
|
||||
}
|
||||
|
||||
removeExtraClass(container, 'animating');
|
||||
toggleExtraClass(container, 'backwards', isBackwards);
|
||||
|
||||
if (name === 'none' || animationLevel === ANIMATION_LEVEL_MIN) {
|
||||
childNodes.forEach((node, i) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
@ -199,57 +199,57 @@ function Transition({
|
||||
});
|
||||
|
||||
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
||||
onStart?.();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
addExtraClass(container, 'animating');
|
||||
addExtraClass(container, 'animating');
|
||||
toggleExtraClass(container, 'backwards', isBackwards);
|
||||
|
||||
onStart?.();
|
||||
function onAnimationEnd() {
|
||||
const activeElement = container.querySelector<HTMLDivElement>(`.${CLASSES.active}`);
|
||||
const { clientHeight } = activeElement || {};
|
||||
|
||||
function onAnimationEnd() {
|
||||
requestAnimationFrame(() => {
|
||||
if (activeKey !== currentKeyRef.current) {
|
||||
return;
|
||||
requestMutation(() => {
|
||||
if (activeKey !== currentKeyRef.current) {
|
||||
return;
|
||||
}
|
||||
|
||||
removeExtraClass(container, 'animating');
|
||||
removeExtraClass(container, 'backwards');
|
||||
|
||||
childNodes.forEach((node, i) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
removeExtraClass(node, 'from');
|
||||
removeExtraClass(node, 'through');
|
||||
removeExtraClass(node, 'to');
|
||||
toggleExtraClass(node, CLASSES.active, i === activeIndex);
|
||||
}
|
||||
|
||||
removeExtraClass(container, 'animating');
|
||||
removeExtraClass(container, 'backwards');
|
||||
|
||||
childNodes.forEach((node, i) => {
|
||||
if (node instanceof HTMLElement) {
|
||||
removeExtraClass(node, 'from');
|
||||
removeExtraClass(node, 'through');
|
||||
removeExtraClass(node, 'to');
|
||||
toggleExtraClass(node, CLASSES.active, i === activeIndex);
|
||||
}
|
||||
});
|
||||
|
||||
if (shouldRestoreHeight) {
|
||||
const activeElement = container.querySelector<HTMLDivElement>(`.${CLASSES.active}`);
|
||||
|
||||
if (activeElement) {
|
||||
activeElement.style.height = 'auto';
|
||||
container.style.height = `${activeElement.clientHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
onStop?.();
|
||||
dispatchHeavyAnimationStop();
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
const watchedNode = name === 'mv-slide'
|
||||
? childNodes[activeIndex]?.firstChild
|
||||
: name === 'reveal' && isBackwards
|
||||
? childNodes[prevActiveIndex]
|
||||
: childNodes[activeIndex];
|
||||
if (shouldRestoreHeight) {
|
||||
if (activeElement) {
|
||||
activeElement.style.height = 'auto';
|
||||
container.style.height = `${clientHeight}px`;
|
||||
}
|
||||
}
|
||||
|
||||
if (watchedNode) {
|
||||
waitForAnimationEnd(watchedNode, onAnimationEnd, undefined, FALLBACK_ANIMATION_END);
|
||||
} else {
|
||||
onAnimationEnd();
|
||||
}
|
||||
});
|
||||
onStop?.();
|
||||
dispatchHeavyAnimationStop();
|
||||
|
||||
cleanup();
|
||||
});
|
||||
}
|
||||
|
||||
const watchedNode = name === 'mv-slide'
|
||||
? childNodes[activeIndex]?.firstChild
|
||||
: name === 'reveal' && isBackwards
|
||||
? childNodes[prevActiveIndex]
|
||||
: childNodes[activeIndex];
|
||||
|
||||
if (watchedNode) {
|
||||
waitForAnimationEnd(watchedNode, onAnimationEnd, undefined, FALLBACK_ANIMATION_END);
|
||||
} else {
|
||||
onAnimationEnd();
|
||||
}
|
||||
}, [
|
||||
activeKey,
|
||||
nextKey,
|
||||
@ -268,20 +268,28 @@ function Transition({
|
||||
forceUpdate,
|
||||
]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
if (shouldRestoreHeight) {
|
||||
const container = containerRef.current!;
|
||||
const activeElement = container.querySelector<HTMLDivElement>(`.${CLASSES.active}`)
|
||||
|| container.querySelector<HTMLDivElement>('.from');
|
||||
const clientHeight = activeElement?.clientHeight;
|
||||
if (!clientHeight) {
|
||||
return;
|
||||
}
|
||||
useEffect(() => {
|
||||
if (!shouldRestoreHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
const container = containerRef.current!;
|
||||
const activeElement = container.querySelector<HTMLDivElement>(`.${CLASSES.active}`)
|
||||
|| container.querySelector<HTMLDivElement>('.from');
|
||||
if (!activeElement) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { clientHeight } = activeElement || {};
|
||||
if (!clientHeight) {
|
||||
return;
|
||||
}
|
||||
|
||||
requestMutation(() => {
|
||||
activeElement.style.height = 'auto';
|
||||
container.style.height = `${clientHeight}px`;
|
||||
container.style.flexBasis = `${clientHeight}px`;
|
||||
}
|
||||
});
|
||||
}, [shouldRestoreHeight, children]);
|
||||
|
||||
const asFastList = !renderCount;
|
||||
@ -360,27 +368,33 @@ function performSlideOptimized(
|
||||
|
||||
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
onStart?.();
|
||||
onStart?.();
|
||||
|
||||
fromSlide.style.transition = 'none';
|
||||
fromSlide.style.transform = 'translate3d(0, 0, 0)';
|
||||
fromSlide.style.transition = 'none';
|
||||
fromSlide.style.transform = 'translate3d(0, 0, 0)';
|
||||
|
||||
toSlide.style.transition = 'none';
|
||||
toSlide.style.transform = `translate3d(${isBackwards ? '-' : ''}100%, 0, 0)`;
|
||||
toSlide.style.transition = 'none';
|
||||
toSlide.style.transform = `translate3d(${isBackwards ? '-' : ''}100%, 0, 0)`;
|
||||
|
||||
requestForcedReflow(() => {
|
||||
forceReflow(toSlide);
|
||||
|
||||
fromSlide.style.transition = '';
|
||||
fromSlide.style.transform = `translate3d(${isBackwards ? '' : '-'}100%, 0, 0)`;
|
||||
return () => {
|
||||
fromSlide.style.transition = '';
|
||||
fromSlide.style.transform = `translate3d(${isBackwards ? '' : '-'}100%, 0, 0)`;
|
||||
|
||||
toSlide.style.transition = '';
|
||||
toSlide.style.transform = 'translate3d(0, 0, 0)';
|
||||
toSlide.style.transition = '';
|
||||
toSlide.style.transform = 'translate3d(0, 0, 0)';
|
||||
|
||||
removeExtraClass(fromSlide, CLASSES.active);
|
||||
addExtraClass(toSlide, CLASSES.active);
|
||||
removeExtraClass(fromSlide, CLASSES.active);
|
||||
addExtraClass(toSlide, CLASSES.active);
|
||||
};
|
||||
});
|
||||
|
||||
waitForTransitionEnd(fromSlide, () => {
|
||||
waitForTransitionEnd(fromSlide, () => {
|
||||
const { clientHeight } = toSlide;
|
||||
|
||||
requestMutation(() => {
|
||||
if (activeKey !== currentKeyRef.current) {
|
||||
return;
|
||||
}
|
||||
@ -390,7 +404,7 @@ function performSlideOptimized(
|
||||
|
||||
if (shouldRestoreHeight) {
|
||||
toSlide.style.height = 'auto';
|
||||
container.style.height = `${toSlide.clientHeight}px`;
|
||||
container.style.height = `${clientHeight}px`;
|
||||
}
|
||||
|
||||
onStop?.();
|
||||
|
||||
@ -8,6 +8,7 @@ export const PRODUCTION_HOSTNAME = 'web.telegram.org';
|
||||
|
||||
export const DEBUG = process.env.APP_ENV !== 'production';
|
||||
export const DEBUG_MORE = false;
|
||||
export const STRICTERDOM_ENABLED = DEBUG;
|
||||
|
||||
export const IS_MOCKED_CLIENT = process.env.APP_MOCKED_CLIENT === '1';
|
||||
export const IS_TEST = process.env.APP_ENV === 'test';
|
||||
@ -143,10 +144,10 @@ export const TMP_CHAT_ID = '0';
|
||||
|
||||
export const ANIMATION_END_DELAY = 100;
|
||||
|
||||
export const FAST_SMOOTH_MAX_DISTANCE = 1500;
|
||||
export const FAST_SMOOTH_MIN_DURATION = 250;
|
||||
export const FAST_SMOOTH_MAX_DURATION = 600;
|
||||
export const FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE = 750; // px
|
||||
export const FAST_SMOOTH_MAX_DISTANCE = 750;
|
||||
export const FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE = 300; // px
|
||||
|
||||
// Average duration of message sending animation
|
||||
export const API_UPDATE_THROTTLE = Math.round((FAST_SMOOTH_MIN_DURATION + FAST_SMOOTH_MAX_DURATION) / 2);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
addActionHandler, getGlobal,
|
||||
setGlobal,
|
||||
@ -52,7 +53,7 @@ export function initializeSoundsForSafari() {
|
||||
sound.currentTime = 0;
|
||||
sound.muted = false;
|
||||
|
||||
requestAnimationFrame(() => {
|
||||
requestNextMutation(() => {
|
||||
sound.src = prevSrc;
|
||||
});
|
||||
});
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
import { requestMutation } from '../../../lib/fasterdom/fasterdom';
|
||||
|
||||
import { addActionHandler, getGlobal, setGlobal } from '../../index';
|
||||
import { ANIMATION_LEVEL_MAX } from '../../../config';
|
||||
import {
|
||||
IS_ANDROID, IS_IOS, IS_MAC_OS, IS_SAFARI, IS_TOUCH_ENV,
|
||||
@ -16,7 +18,6 @@ import { callApi } from '../../../api/gramjs';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
import { updateTabState } from '../../reducers/tabs';
|
||||
import { getCurrentTabId } from '../../../util/establishMultitabRole';
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
|
||||
const HISTORY_ANIMATION_DURATION = 450;
|
||||
|
||||
@ -103,31 +104,33 @@ addCallback((global: GlobalState) => {
|
||||
|
||||
void setLanguage(language, undefined, true);
|
||||
|
||||
document.documentElement.style.setProperty(
|
||||
'--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(messageTextSize * 1.3125)}px`);
|
||||
document.documentElement.style.setProperty('--message-text-size', `${messageTextSize}px`);
|
||||
document.documentElement.setAttribute('data-message-text-size', messageTextSize.toString());
|
||||
document.body.classList.add('initial');
|
||||
document.body.classList.add(`animation-level-${animationLevel}`);
|
||||
document.body.classList.add(IS_TOUCH_ENV ? 'is-touch-env' : 'is-pointer-env');
|
||||
requestMutation(() => {
|
||||
document.documentElement.style.setProperty(
|
||||
'--composer-text-size', `${Math.max(messageTextSize, IS_IOS ? 16 : 15)}px`,
|
||||
);
|
||||
document.documentElement.style.setProperty('--message-meta-height', `${Math.floor(messageTextSize * 1.3125)}px`);
|
||||
document.documentElement.style.setProperty('--message-text-size', `${messageTextSize}px`);
|
||||
document.documentElement.setAttribute('data-message-text-size', messageTextSize.toString());
|
||||
document.body.classList.add('initial');
|
||||
document.body.classList.add(`animation-level-${animationLevel}`);
|
||||
document.body.classList.add(IS_TOUCH_ENV ? 'is-touch-env' : 'is-pointer-env');
|
||||
|
||||
if (IS_IOS) {
|
||||
document.body.classList.add('is-ios');
|
||||
} else if (IS_ANDROID) {
|
||||
document.body.classList.add('is-android');
|
||||
} else if (IS_MAC_OS) {
|
||||
document.body.classList.add('is-macos');
|
||||
}
|
||||
if (IS_SAFARI) {
|
||||
document.body.classList.add('is-safari');
|
||||
}
|
||||
});
|
||||
|
||||
switchTheme(theme, animationLevel === ANIMATION_LEVEL_MAX);
|
||||
|
||||
startWebsync();
|
||||
|
||||
if (IS_IOS) {
|
||||
document.body.classList.add('is-ios');
|
||||
} else if (IS_ANDROID) {
|
||||
document.body.classList.add('is-android');
|
||||
} else if (IS_MAC_OS) {
|
||||
document.body.classList.add('is-macos');
|
||||
}
|
||||
if (IS_SAFARI) {
|
||||
document.body.classList.add('is-safari');
|
||||
}
|
||||
|
||||
isUpdated = true;
|
||||
|
||||
if (isUpdated) setGlobal(global);
|
||||
@ -144,7 +147,9 @@ addActionHandler('setIsUiReady', (global, actions, payload): ActionReturnType =>
|
||||
const { uiReadyState, tabId = getCurrentTabId() } = payload!;
|
||||
|
||||
if (uiReadyState === 2) {
|
||||
document.body.classList.remove('initial');
|
||||
requestMutation(() => {
|
||||
document.body.classList.remove('initial');
|
||||
});
|
||||
}
|
||||
|
||||
return updateTabState(global, {
|
||||
@ -184,7 +189,10 @@ addActionHandler('disableHistoryAnimations', (global, actions, payload): ActionR
|
||||
shouldSkipHistoryAnimations: false,
|
||||
}, tabId);
|
||||
setGlobal(global);
|
||||
document.body.classList.remove('no-animate');
|
||||
|
||||
requestMutation(() => {
|
||||
document.body.classList.remove('no-animate');
|
||||
});
|
||||
}, HISTORY_ANIMATION_DURATION);
|
||||
|
||||
global = updateTabState(global, {
|
||||
|
||||
@ -1,5 +1,8 @@
|
||||
import type { Signal } from '../util/signals';
|
||||
|
||||
import useThrottledCallback from './useThrottledCallback';
|
||||
import useDebouncedCallback from './useDebouncedCallback';
|
||||
import useDerivedSignal from './useDerivedSignal';
|
||||
|
||||
export function useThrottledResolver<T>(resolver: () => T, deps: any[], ms: number, noFirst = false) {
|
||||
return useThrottledCallback((setValue: (newValue: T) => void) => {
|
||||
@ -14,3 +17,16 @@ export function useDebouncedResolver<T>(resolver: () => T, deps: any[], ms: numb
|
||||
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
||||
}, deps, ms, noFirst, noLast);
|
||||
}
|
||||
|
||||
export function useDebouncedSignal<T extends any>(
|
||||
getValue: Signal<T>,
|
||||
ms: number,
|
||||
noFirst = false,
|
||||
noLast = false,
|
||||
): Signal<T> {
|
||||
const debouncedResolver = useDebouncedResolver(() => getValue(), [getValue], ms, noFirst, noLast);
|
||||
|
||||
return useDerivedSignal(
|
||||
debouncedResolver, [debouncedResolver, getValue], true,
|
||||
);
|
||||
}
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
import {
|
||||
useCallback, useLayoutEffect, useMemo, useState,
|
||||
useCallback, useEffect, useMemo, useState,
|
||||
} from '../lib/teact/teact';
|
||||
import { requestMeasure } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import { round } from '../util/math';
|
||||
|
||||
@ -27,7 +28,8 @@ export default function useBoundsInSharedCanvas(
|
||||
|
||||
// Wait until elements are properly mounted
|
||||
if (!container.offsetParent || !canvas.offsetParent) {
|
||||
setTimeout(recalculate, ANIMATION_END_TIMEOUT);
|
||||
// `requestMeasure` is useful as timeouts are run in parallel with image loadings and thus causing reflow
|
||||
setTimeout(() => requestMeasure(recalculate), ANIMATION_END_TIMEOUT);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -38,15 +40,14 @@ export default function useBoundsInSharedCanvas(
|
||||
const canvasBounds = canvas.getBoundingClientRect();
|
||||
|
||||
// Factor coords are used to support rendering while being rescaled (e.g. message appearance animation)
|
||||
setX(round((targetBounds.left - canvasBounds.left) / canvasBounds.width, 4));
|
||||
setY(round((targetBounds.top - canvasBounds.top) / canvasBounds.height, 4));
|
||||
setX(round((targetBounds.left - canvasBounds.left) / canvasBounds.width, 4) || 0);
|
||||
setY(round((targetBounds.top - canvasBounds.top) / canvasBounds.height, 4) || 0);
|
||||
setSize(Math.round(targetBounds.width));
|
||||
}, [containerRef, sharedCanvasRef]);
|
||||
|
||||
const throttledRecalculate = useThrottledCallback(recalculate, [recalculate], THROTTLE_MS, false);
|
||||
|
||||
useLayoutEffect(recalculate, [recalculate]);
|
||||
useEffect(recalculate, [recalculate]);
|
||||
|
||||
const throttledRecalculate = useThrottledCallback(recalculate, [recalculate], THROTTLE_MS);
|
||||
useResizeObserver(sharedCanvasRef, throttledRecalculate);
|
||||
|
||||
const coords = useMemo(() => (x !== undefined && y !== undefined ? { x, y } : undefined), [x, y]);
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { useEffect, useRef } from '../lib/teact/teact';
|
||||
import { requestMeasure, requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import { IS_CANVAS_FILTER_SUPPORTED } from '../util/windowEnvironment';
|
||||
import fastBlur from '../lib/fastBlur';
|
||||
@ -37,25 +38,29 @@ export default function useCanvasBlur(
|
||||
const img = new Image();
|
||||
|
||||
const processBlur = () => {
|
||||
canvas.width = preferredWidth || img.width;
|
||||
canvas.height = preferredHeight || img.height;
|
||||
|
||||
const width = preferredWidth || img.width;
|
||||
const height = preferredHeight || img.height;
|
||||
const ctx = canvas.getContext('2d', { alpha: false })!;
|
||||
|
||||
if (IS_CANVAS_FILTER_SUPPORTED) {
|
||||
ctx.filter = `blur(${radius}px)`;
|
||||
}
|
||||
requestMutation(() => {
|
||||
canvas.width = width;
|
||||
canvas.height = height;
|
||||
|
||||
ctx.drawImage(img, -radius * 2, -radius * 2, canvas.width + radius * 4, canvas.height + radius * 4);
|
||||
if (IS_CANVAS_FILTER_SUPPORTED) {
|
||||
ctx.filter = `blur(${radius}px)`;
|
||||
}
|
||||
|
||||
if (!IS_CANVAS_FILTER_SUPPORTED) {
|
||||
fastBlur(ctx, 0, 0, canvas.width, canvas.height, radius, ITERATIONS);
|
||||
}
|
||||
ctx.drawImage(img, -radius * 2, -radius * 2, width + radius * 4, height + radius * 4);
|
||||
|
||||
if (!IS_CANVAS_FILTER_SUPPORTED) {
|
||||
fastBlur(ctx, 0, 0, width, height, radius, ITERATIONS);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
img.onload = () => {
|
||||
if (withRaf) {
|
||||
requestAnimationFrame(processBlur);
|
||||
requestMeasure(processBlur);
|
||||
} else {
|
||||
processBlur();
|
||||
}
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useState, useEffect, useCallback } from '../lib/teact/teact';
|
||||
import { addExtraClass, removeExtraClass } from '../lib/teact/teact-dom';
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { IAnchorPosition } from '../types';
|
||||
import {
|
||||
@ -26,12 +28,16 @@ const useContextMenuHandlers = (
|
||||
|
||||
const handleBeforeContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
if (!isMenuDisabled && e.button === 2) {
|
||||
(e.target as HTMLElement).classList.add('no-selection');
|
||||
requestMutation(() => {
|
||||
addExtraClass(e.target as HTMLElement, 'no-selection');
|
||||
});
|
||||
}
|
||||
}, [isMenuDisabled]);
|
||||
|
||||
const handleContextMenu = useCallback((e: React.MouseEvent) => {
|
||||
(e.target as HTMLElement).classList.remove('no-selection');
|
||||
requestMutation(() => {
|
||||
removeExtraClass(e.target as HTMLElement, 'no-selection');
|
||||
});
|
||||
|
||||
if (isMenuDisabled || (shouldDisableOnLink && (e.target as HTMLElement).matches('a[href]'))) {
|
||||
return;
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
import {
|
||||
useCallback, useEffect, useRef, useState,
|
||||
useCallback, useEffect, useLayoutEffect, useRef, useState,
|
||||
} from '../lib/teact/teact';
|
||||
import { hexToRgb } from '../util/switchTheme';
|
||||
import { getPropertyHexColor } from '../util/themeStyle';
|
||||
@ -41,8 +41,22 @@ export default function useDynamicColorListener(ref?: React.RefObject<HTMLElemen
|
||||
rgbColorRef.current = [r, g, b];
|
||||
}, [hexColor]);
|
||||
|
||||
useLayoutEffect(() => {
|
||||
const el = ref?.current;
|
||||
if (!el || isDisabled) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
el.style.setProperty('transition', TRANSITION_STYLE, 'important');
|
||||
|
||||
return () => {
|
||||
el.style.removeProperty('transition');
|
||||
};
|
||||
}, [isDisabled, ref]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!ref?.current) {
|
||||
const el = ref?.current;
|
||||
if (!el) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
@ -57,12 +71,10 @@ export default function useDynamicColorListener(ref?: React.RefObject<HTMLElemen
|
||||
updateColor();
|
||||
}
|
||||
|
||||
const el = ref.current;
|
||||
el.addEventListener('transitionend', handleTransitionEnd);
|
||||
el.style.setProperty('transition', TRANSITION_STYLE, 'important');
|
||||
|
||||
return () => {
|
||||
el.removeEventListener('transitionend', handleTransitionEnd);
|
||||
el.style.removeProperty('transition');
|
||||
};
|
||||
}, [isDisabled, ref, updateColor]);
|
||||
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import { IS_TOUCH_ENV } from '../util/windowEnvironment';
|
||||
import { fastRaf } from '../util/schedulers';
|
||||
import { useEffect } from '../lib/teact/teact';
|
||||
|
||||
const DEFAULT_DURATION = 400;
|
||||
@ -15,10 +15,8 @@ export default function useFocusAfterAnimation(
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
fastRaf(() => {
|
||||
if (ref.current) {
|
||||
ref.current.focus();
|
||||
}
|
||||
requestMutation(() => {
|
||||
ref.current?.focus();
|
||||
});
|
||||
}, animationDuration);
|
||||
}, [ref, animationDuration]);
|
||||
|
||||
@ -1,8 +1,8 @@
|
||||
import { useCallback, useRef } from '../lib/teact/teact';
|
||||
import { getActions } from '../lib/teact/teactn';
|
||||
import { requestMeasure } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import { IS_TEST } from '../config';
|
||||
import { fastRaf } from '../util/schedulers';
|
||||
import { IS_IOS } from '../util/windowEnvironment';
|
||||
|
||||
import useSyncEffect from './useSyncEffect';
|
||||
@ -109,7 +109,10 @@ function processStateOperations(stateOperations: HistoryOperationState[]) {
|
||||
}
|
||||
|
||||
function deferHistoryOperation(historyOperation: HistoryOperation) {
|
||||
if (!deferredHistoryOperations.length) fastRaf(applyDeferredHistoryOperations);
|
||||
if (!deferredHistoryOperations.length) {
|
||||
requestMeasure(applyDeferredHistoryOperations);
|
||||
}
|
||||
|
||||
deferredHistoryOperations.push(historyOperation);
|
||||
}
|
||||
|
||||
|
||||
@ -1,5 +1,7 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from '../lib/teact/teact';
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import useAppLayout from './useAppLayout';
|
||||
|
||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
||||
@ -17,7 +19,7 @@ export default function useInputFocusOnOpen(
|
||||
if (isOpen) {
|
||||
if (!isMobile) {
|
||||
setTimeout(() => {
|
||||
requestAnimationFrame(() => {
|
||||
requestMutation(() => {
|
||||
if (inputRef.current?.isConnected) {
|
||||
inputRef.current.focus();
|
||||
}
|
||||
|
||||
@ -3,7 +3,11 @@ import {
|
||||
useEffect, useRef, useCallback, useState,
|
||||
} from '../lib/teact/teact';
|
||||
|
||||
import { throttle, debounce } from '../util/schedulers';
|
||||
import type { Scheduler } from '../util/schedulers';
|
||||
|
||||
import {
|
||||
throttle, debounce, throttleWith,
|
||||
} from '../util/schedulers';
|
||||
import useEffectOnce from './useEffectOnce';
|
||||
import useHeavyAnimationCheck from './useHeavyAnimationCheck';
|
||||
|
||||
@ -26,6 +30,7 @@ interface Response {
|
||||
export function useIntersectionObserver({
|
||||
rootRef,
|
||||
throttleMs,
|
||||
throttleScheduler,
|
||||
debounceMs,
|
||||
shouldSkipFirst,
|
||||
margin,
|
||||
@ -34,6 +39,7 @@ export function useIntersectionObserver({
|
||||
}: {
|
||||
rootRef: RefObject<HTMLDivElement>;
|
||||
throttleMs?: number;
|
||||
throttleScheduler?: Scheduler;
|
||||
debounceMs?: number;
|
||||
shouldSkipFirst?: boolean;
|
||||
margin?: number;
|
||||
@ -99,10 +105,18 @@ export function useIntersectionObserver({
|
||||
|
||||
entriesAccumulator.clear();
|
||||
};
|
||||
const scheduler = throttleMs ? throttle : debounceMs ? debounce : undefined;
|
||||
const observerCallback = scheduler
|
||||
? scheduler(observerCallbackSync, (throttleMs || debounceMs)!, !shouldSkipFirst)
|
||||
: observerCallbackSync;
|
||||
|
||||
let observerCallback: typeof observerCallbackSync;
|
||||
if (typeof throttleScheduler === 'function') {
|
||||
observerCallback = throttleWith(throttleScheduler, observerCallbackSync);
|
||||
} else if (throttleMs) {
|
||||
observerCallback = throttle(observerCallbackSync, throttleMs, !shouldSkipFirst);
|
||||
} else if (debounceMs) {
|
||||
observerCallback = debounce(observerCallbackSync, debounceMs, !shouldSkipFirst);
|
||||
} else {
|
||||
observerCallback = observerCallbackSync;
|
||||
}
|
||||
|
||||
const observer = new IntersectionObserver(
|
||||
(entries) => {
|
||||
entries.forEach((entry) => {
|
||||
|
||||
@ -16,8 +16,8 @@ export default function useResizeObserver(
|
||||
}
|
||||
const el = ref.current;
|
||||
const callback: ResizeObserverCallback = ([entry]) => {
|
||||
// During animation
|
||||
if (!(entry.target as HTMLElement).offsetParent) {
|
||||
// Ignore updates when element is not properly mounted (`display: none`)
|
||||
if (entry.contentRect.width === 0 && entry.contentRect.height === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,22 +1,22 @@
|
||||
import { useCallback, useMemo } from '../lib/teact/teact';
|
||||
|
||||
import type { fastRaf } from '../util/schedulers';
|
||||
import { throttle, throttleWithRaf } from '../util/schedulers';
|
||||
import type { Scheduler } from '../util/schedulers';
|
||||
import { throttle, throttleWith } from '../util/schedulers';
|
||||
|
||||
export default function useThrottledCallback<T extends AnyToVoidFunction>(
|
||||
fn: T,
|
||||
deps: any[],
|
||||
msOrRaf: number | typeof fastRaf,
|
||||
msOrSchedulerFn: number | Scheduler,
|
||||
noFirst = false,
|
||||
) {
|
||||
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
||||
const fnMemo = useCallback(fn, deps);
|
||||
|
||||
return useMemo(() => {
|
||||
if (typeof msOrRaf === 'number') {
|
||||
return throttle(fnMemo, msOrRaf, !noFirst);
|
||||
if (typeof msOrSchedulerFn === 'number') {
|
||||
return throttle(fnMemo, msOrSchedulerFn, !noFirst);
|
||||
} else {
|
||||
return throttleWithRaf(fnMemo);
|
||||
return throttleWith(msOrSchedulerFn, fnMemo);
|
||||
}
|
||||
}, [fnMemo, msOrRaf, noFirst]);
|
||||
}, [fnMemo, msOrSchedulerFn, noFirst]);
|
||||
}
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
import type { RefObject } from 'react';
|
||||
import { useEffect } from '../lib/teact/teact';
|
||||
import { fastRaf } from '../util/schedulers';
|
||||
import { requestNextMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
// Fix for memory leak when unmounting video element
|
||||
export default function useVideoCleanup(videoRef: RefObject<HTMLVideoElement>, dependencies: any[]) {
|
||||
@ -9,7 +9,8 @@ export default function useVideoCleanup(videoRef: RefObject<HTMLVideoElement>, d
|
||||
|
||||
return () => {
|
||||
if (videoEl) {
|
||||
fastRaf(() => {
|
||||
// It may be slow (specifically on iOS), so we postpone it after unmounting
|
||||
requestNextMutation(() => {
|
||||
videoEl.pause();
|
||||
videoEl.src = '';
|
||||
videoEl.load();
|
||||
|
||||
@ -3,6 +3,7 @@ import './util/setupServiceWorker';
|
||||
|
||||
import React from './lib/teact/teact';
|
||||
import TeactDOM from './lib/teact/teact-dom';
|
||||
import { enableStrict, requestMutation } from './lib/fasterdom/fasterdom';
|
||||
|
||||
import {
|
||||
getActions, getGlobal,
|
||||
@ -11,7 +12,9 @@ import updateWebmanifest from './util/updateWebmanifest';
|
||||
import { IS_MULTITAB_SUPPORTED } from './util/windowEnvironment';
|
||||
import './global/init';
|
||||
|
||||
import { APP_VERSION, DEBUG, MULTITAB_LOCALSTORAGE_KEY } from './config';
|
||||
import {
|
||||
APP_VERSION, DEBUG, MULTITAB_LOCALSTORAGE_KEY, STRICTERDOM_ENABLED,
|
||||
} from './config';
|
||||
import { establishMultitabRole, subscribeToMasterChange } from './util/establishMultitabRole';
|
||||
import { requestGlobal, subscribeToMultitabBroadcastChannel } from './util/multitab';
|
||||
import { onBeforeUnload } from './util/schedulers';
|
||||
@ -21,6 +24,12 @@ import App from './components/App';
|
||||
|
||||
import './styles/index.scss';
|
||||
|
||||
if (STRICTERDOM_ENABLED) {
|
||||
enableStrict();
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
async function init() {
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -56,12 +65,14 @@ async function init() {
|
||||
console.log('>>> START INITIAL RENDER');
|
||||
}
|
||||
|
||||
updateWebmanifest();
|
||||
requestMutation(() => {
|
||||
updateWebmanifest();
|
||||
|
||||
TeactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('root')!,
|
||||
);
|
||||
TeactDOM.render(
|
||||
<App />,
|
||||
document.getElementById('root')!,
|
||||
);
|
||||
});
|
||||
|
||||
if (DEBUG) {
|
||||
// eslint-disable-next-line no-console
|
||||
@ -77,5 +88,3 @@ async function init() {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
init();
|
||||
|
||||
86
src/lib/fasterdom/fasterdom.ts
Normal file
86
src/lib/fasterdom/fasterdom.ts
Normal file
@ -0,0 +1,86 @@
|
||||
import { fastRaf, throttleWith } from '../../util/schedulers';
|
||||
import safeExec from '../../util/safeExec';
|
||||
|
||||
import { setPhase } from './stricterdom';
|
||||
|
||||
let pendingMeasureTasks: NoneToVoidFunction[] = [];
|
||||
let pendingMutationTasks: NoneToVoidFunction[] = [];
|
||||
let pendingForceReflowTasks: (() => NoneToVoidFunction | void)[] = [];
|
||||
|
||||
const runUpdatePassOnRaf = throttleWithRafFallback(() => {
|
||||
const currentMeasureTasks = pendingMeasureTasks;
|
||||
pendingMeasureTasks = [];
|
||||
currentMeasureTasks.forEach((task) => {
|
||||
safeExec(task);
|
||||
});
|
||||
|
||||
// We use promises to provide correct order for Mutation Observer callback microtasks
|
||||
Promise.resolve()
|
||||
.then(() => {
|
||||
setPhase('mutate');
|
||||
|
||||
const currentMutationTasks = pendingMutationTasks;
|
||||
pendingMutationTasks = [];
|
||||
|
||||
currentMutationTasks.forEach((task) => {
|
||||
safeExec(task);
|
||||
});
|
||||
})
|
||||
.then(() => {
|
||||
setPhase('measure');
|
||||
|
||||
const pendingForceReflowMutationTasks: NoneToVoidFunction[] = [];
|
||||
// Will include tasks created during the loop
|
||||
for (const task of pendingForceReflowTasks) {
|
||||
safeExec(() => {
|
||||
const mutationTask = task();
|
||||
if (mutationTask) {
|
||||
pendingForceReflowMutationTasks.push(mutationTask);
|
||||
}
|
||||
});
|
||||
}
|
||||
pendingForceReflowTasks = [];
|
||||
|
||||
return pendingForceReflowMutationTasks;
|
||||
})
|
||||
.then((pendingForceReflowMutationTasks) => {
|
||||
setPhase('mutate');
|
||||
|
||||
// Will include tasks created during the loop
|
||||
for (const task of pendingForceReflowMutationTasks) {
|
||||
safeExec(task);
|
||||
}
|
||||
})
|
||||
.then(() => {
|
||||
setPhase('measure');
|
||||
});
|
||||
});
|
||||
|
||||
export function requestMeasure(cb: NoneToVoidFunction) {
|
||||
pendingMeasureTasks.push(cb);
|
||||
runUpdatePassOnRaf();
|
||||
}
|
||||
|
||||
export function requestMutation(cb: NoneToVoidFunction) {
|
||||
pendingMutationTasks.push(cb);
|
||||
runUpdatePassOnRaf();
|
||||
}
|
||||
|
||||
export function requestNextMutation(cb: () => (NoneToVoidFunction | void)) {
|
||||
requestMeasure(() => {
|
||||
requestMutation(cb);
|
||||
});
|
||||
}
|
||||
|
||||
export function requestForcedReflow(cb: () => (NoneToVoidFunction | void)) {
|
||||
pendingForceReflowTasks.push(cb);
|
||||
runUpdatePassOnRaf();
|
||||
}
|
||||
|
||||
function throttleWithRafFallback<F extends AnyToVoidFunction>(fn: F) {
|
||||
return throttleWith((throttledFn: NoneToVoidFunction) => {
|
||||
fastRaf(throttledFn, true);
|
||||
}, fn);
|
||||
}
|
||||
|
||||
export * from './stricterdom';
|
||||
46
src/lib/fasterdom/layoutCauses.ts
Normal file
46
src/lib/fasterdom/layoutCauses.ts
Normal file
@ -0,0 +1,46 @@
|
||||
// https://gist.github.com/paulirish/5d52fb081b3570c81e3a
|
||||
|
||||
export default {
|
||||
Element: {
|
||||
props: [
|
||||
'clientLeft', 'clientTop', 'clientWidth', 'clientHeight',
|
||||
'scrollWidth', 'scrollHeight', 'scrollLeft', 'scrollTop',
|
||||
] as const,
|
||||
methods: [
|
||||
'getClientRects', 'getBoundingClientRect',
|
||||
'scrollBy', 'scrollTo', 'scrollIntoView', 'scrollIntoViewIfNeeded',
|
||||
] as const,
|
||||
},
|
||||
HTMLElement: {
|
||||
props: [
|
||||
'offsetLeft', 'offsetTop', 'offsetWidth', 'offsetHeight', 'offsetParent',
|
||||
'innerText',
|
||||
] as const,
|
||||
methods: ['focus'] as const,
|
||||
},
|
||||
window: {
|
||||
props: [
|
||||
'scrollX', 'scrollY',
|
||||
'innerHeight', 'innerWidth',
|
||||
] as const,
|
||||
methods: ['getComputedStyle'] as const,
|
||||
},
|
||||
VisualViewport: {
|
||||
props: [
|
||||
'height', 'width', 'offsetTop', 'offsetLeft',
|
||||
] as const,
|
||||
},
|
||||
Document: {
|
||||
props: ['scrollingElement'] as const,
|
||||
methods: ['elementFromPoint'] as const,
|
||||
},
|
||||
HTMLInputElement: {
|
||||
methods: ['select'] as const,
|
||||
},
|
||||
MouseEvent: {
|
||||
props: ['layerX', 'layerY', 'offsetX', 'offsetY'] as const,
|
||||
},
|
||||
Range: {
|
||||
methods: ['getClientRects', 'getBoundingClientRect'] as const,
|
||||
},
|
||||
};
|
||||
165
src/lib/fasterdom/stricterdom.ts
Normal file
165
src/lib/fasterdom/stricterdom.ts
Normal file
@ -0,0 +1,165 @@
|
||||
import LAYOUT_CAUSES from './layoutCauses';
|
||||
|
||||
type Entities = keyof typeof LAYOUT_CAUSES;
|
||||
type Phase =
|
||||
'measure'
|
||||
| 'mutate';
|
||||
type ErrorHandler = (error: Error) => any;
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
const DEFAULT_ERROR_HANDLER = console.error;
|
||||
|
||||
let onError: ErrorHandler = DEFAULT_ERROR_HANDLER;
|
||||
|
||||
const nativeMethods = new Map<string, AnyFunction>();
|
||||
|
||||
let phase: Phase = 'measure';
|
||||
let isStrict = false;
|
||||
let observer: MutationObserver | undefined;
|
||||
|
||||
export function setPhase(newPhase: Phase) {
|
||||
phase = newPhase;
|
||||
}
|
||||
|
||||
export function getPhase() {
|
||||
return phase;
|
||||
}
|
||||
|
||||
export function enableStrict() {
|
||||
if (isStrict) return;
|
||||
|
||||
isStrict = true;
|
||||
setupLayoutDetectors();
|
||||
setupMutationObserver();
|
||||
}
|
||||
|
||||
export function disableStrict() {
|
||||
if (!isStrict) return;
|
||||
|
||||
clearMutationObserver();
|
||||
clearLayoutDetectors();
|
||||
isStrict = false;
|
||||
}
|
||||
|
||||
export function forceMeasure(cb: () => any) {
|
||||
if (phase !== 'mutate') {
|
||||
throw new Error('The current phase is \'measure\'');
|
||||
}
|
||||
|
||||
phase = 'measure';
|
||||
const result = cb();
|
||||
phase = 'mutate';
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
export function setHandler(handler?: ErrorHandler) {
|
||||
onError = handler || DEFAULT_ERROR_HANDLER;
|
||||
}
|
||||
|
||||
function setupLayoutDetectors() {
|
||||
Object.entries(LAYOUT_CAUSES).forEach(([name, causes]) => {
|
||||
const entity = window[name as Entities];
|
||||
const prototype = typeof entity === 'object' ? entity : entity.prototype;
|
||||
|
||||
if ('props' in causes) {
|
||||
causes.props.forEach((prop) => {
|
||||
const nativeGetter = Object.getOwnPropertyDescriptor(prototype, prop)?.get;
|
||||
if (!nativeGetter) {
|
||||
return;
|
||||
}
|
||||
|
||||
nativeMethods.set(`${name}#${prop}`, nativeGetter);
|
||||
|
||||
Object.defineProperty(prototype, prop, {
|
||||
get() {
|
||||
onMeasure(prop);
|
||||
|
||||
return nativeGetter.call(this);
|
||||
},
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if ('methods' in causes) {
|
||||
causes.methods.forEach((method) => {
|
||||
const nativeMethod = (prototype as any)[method]!;
|
||||
nativeMethods.set(`${name}#${method}`, nativeMethod);
|
||||
|
||||
// eslint-disable-next-line func-names
|
||||
(prototype as any)[method] = function (...args: any[]) {
|
||||
onMeasure(method);
|
||||
|
||||
return nativeMethod.apply(this, args);
|
||||
};
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function clearLayoutDetectors() {
|
||||
Object.entries(LAYOUT_CAUSES).forEach(([name, causes]) => {
|
||||
const entity = window[name as Entities];
|
||||
const prototype = typeof entity === 'object' ? entity : entity.prototype;
|
||||
|
||||
if ('props' in causes) {
|
||||
causes.props.forEach((prop) => {
|
||||
const nativeGetter = nativeMethods.get(`${name}#${prop}`);
|
||||
if (!nativeGetter) {
|
||||
return;
|
||||
}
|
||||
|
||||
Object.defineProperty(prototype, prop, { get: nativeGetter });
|
||||
});
|
||||
}
|
||||
|
||||
if ('methods' in causes) {
|
||||
causes.methods.forEach((method) => {
|
||||
(prototype as any)[method] = nativeMethods.get(`${name}#${method}`)!;
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
nativeMethods.clear();
|
||||
}
|
||||
|
||||
function setupMutationObserver() {
|
||||
observer = new MutationObserver((mutations) => {
|
||||
if (phase !== 'mutate') {
|
||||
mutations.forEach(({ target, type, attributeName }) => {
|
||||
if (!document.contains(target)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (type === 'childList' && target instanceof HTMLElement && target.contentEditable) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (attributeName?.startsWith('data-')) {
|
||||
return;
|
||||
}
|
||||
|
||||
// eslint-disable-next-line no-console
|
||||
onError(new Error(`Unexpected mutation detected: \`${type === 'attributes' ? attributeName : type}\``));
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
observer.observe(document.body, {
|
||||
childList: true,
|
||||
attributes: true,
|
||||
subtree: true,
|
||||
characterData: false,
|
||||
});
|
||||
}
|
||||
|
||||
function clearMutationObserver() {
|
||||
observer?.disconnect();
|
||||
observer = undefined;
|
||||
}
|
||||
|
||||
function onMeasure(propName: string) {
|
||||
if (phase !== 'measure') {
|
||||
onError(new Error(`Unexpected measurement detected: \`${propName}\``));
|
||||
}
|
||||
}
|
||||
@ -1,3 +1,5 @@
|
||||
import { requestMeasure, requestMutation } from '../fasterdom/fasterdom';
|
||||
|
||||
import type { RLottieApi } from './rlottie.worker';
|
||||
|
||||
import {
|
||||
@ -6,7 +8,6 @@ import {
|
||||
import { createConnector } from '../../util/PostMessageConnector';
|
||||
import { animate } from '../../util/animation';
|
||||
import cycleRestrict from '../../util/cycleRestrict';
|
||||
import { fastRaf } from '../../util/schedulers';
|
||||
import generateIdFor from '../../util/generateIdFor';
|
||||
|
||||
interface Params {
|
||||
@ -222,25 +223,29 @@ class RLottie {
|
||||
canvas, ctx,
|
||||
} = containerInfo;
|
||||
|
||||
let [canvasWidth, canvasHeight] = [canvas.width, canvas.height];
|
||||
|
||||
if (!canvas.dataset.isJustCleaned || canvas.dataset.isJustCleaned === 'false') {
|
||||
const sizeFactor = this.calcSizeFactor();
|
||||
ensureCanvasSize(canvas, sizeFactor);
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
([canvasWidth, canvasHeight] = ensureCanvasSize(canvas, sizeFactor));
|
||||
ctx.clearRect(0, 0, canvasWidth, canvasHeight);
|
||||
canvas.dataset.isJustCleaned = 'true';
|
||||
fastRaf(() => {
|
||||
requestMeasure(() => {
|
||||
canvas.dataset.isJustCleaned = 'false';
|
||||
});
|
||||
}
|
||||
|
||||
containerInfo.coords = {
|
||||
x: Math.round((newCoords?.x || 0) * canvas.width),
|
||||
y: Math.round((newCoords?.y || 0) * canvas.height),
|
||||
x: Math.round((newCoords?.x || 0) * canvasWidth),
|
||||
y: Math.round((newCoords?.y || 0) * canvasHeight),
|
||||
};
|
||||
|
||||
const frame = this.getFrame(this.prevFrameIndex) || this.getFrame(Math.round(this.approxFrameIndex));
|
||||
|
||||
if (frame && frame !== WAITING) {
|
||||
ctx.drawImage(frame, containerInfo.coords.x, containerInfo.coords.y);
|
||||
requestMutation(() => {
|
||||
ctx.drawImage(frame, containerInfo.coords!.x, containerInfo.coords!.y);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@ -273,21 +278,28 @@ class RLottie {
|
||||
}
|
||||
}
|
||||
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
canvas.style.width = `${size}px`;
|
||||
canvas.style.height = `${size}px`;
|
||||
|
||||
imgSize = Math.round(size * sizeFactor);
|
||||
|
||||
canvas.width = imgSize;
|
||||
canvas.height = imgSize;
|
||||
if (!this.imgSize) {
|
||||
this.imgSize = imgSize;
|
||||
this.imageData = new ImageData(imgSize, imgSize);
|
||||
}
|
||||
|
||||
container.appendChild(canvas);
|
||||
requestMutation(() => {
|
||||
const canvas = document.createElement('canvas');
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
this.views.set(viewId, {
|
||||
canvas, ctx, onLoad,
|
||||
canvas.style.width = `${size}px`;
|
||||
canvas.style.height = `${size}px`;
|
||||
|
||||
canvas.width = imgSize;
|
||||
canvas.height = imgSize;
|
||||
|
||||
container.appendChild(canvas);
|
||||
|
||||
this.views.set(viewId, {
|
||||
canvas, ctx, onLoad,
|
||||
});
|
||||
});
|
||||
} else {
|
||||
if (!container.isConnected) {
|
||||
@ -297,27 +309,27 @@ class RLottie {
|
||||
const canvas = container;
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
|
||||
ensureCanvasSize(canvas, sizeFactor);
|
||||
|
||||
imgSize = Math.round(this.params.size! * sizeFactor);
|
||||
|
||||
if (!this.imgSize) {
|
||||
this.imgSize = imgSize;
|
||||
this.imageData = new ImageData(imgSize, imgSize);
|
||||
}
|
||||
|
||||
const [canvasWidth, canvasHeight] = ensureCanvasSize(canvas, sizeFactor);
|
||||
|
||||
this.views.set(viewId, {
|
||||
canvas,
|
||||
ctx,
|
||||
isSharedCanvas: true,
|
||||
coords: {
|
||||
x: Math.round((coords?.x || 0) * canvas.width),
|
||||
y: Math.round((coords?.y || 0) * canvas.height),
|
||||
x: Math.round((coords?.x || 0) * canvasWidth),
|
||||
y: Math.round((coords?.y || 0) * canvasHeight),
|
||||
},
|
||||
onLoad,
|
||||
});
|
||||
}
|
||||
|
||||
if (!this.imgSize) {
|
||||
this.imgSize = imgSize;
|
||||
this.imageData = new ImageData(imgSize, imgSize);
|
||||
}
|
||||
|
||||
if (this.isRendererInited) {
|
||||
this.doPlay();
|
||||
}
|
||||
@ -556,7 +568,7 @@ class RLottie {
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}, requestMutation);
|
||||
}
|
||||
|
||||
private getFrame(frameIndex: number) {
|
||||
@ -597,10 +609,15 @@ class RLottie {
|
||||
function ensureCanvasSize(canvas: HTMLCanvasElement, sizeFactor: number) {
|
||||
const expectedWidth = Math.round(canvas.offsetWidth * sizeFactor);
|
||||
const expectedHeight = Math.round(canvas.offsetHeight * sizeFactor);
|
||||
|
||||
if (canvas.width !== expectedWidth || canvas.height !== expectedHeight) {
|
||||
canvas.width = expectedWidth;
|
||||
canvas.height = expectedHeight;
|
||||
requestMutation(() => {
|
||||
canvas.width = expectedWidth;
|
||||
canvas.height = expectedHeight;
|
||||
});
|
||||
}
|
||||
|
||||
return [expectedWidth, expectedHeight];
|
||||
}
|
||||
|
||||
export default RLottie;
|
||||
|
||||
@ -1,11 +1,13 @@
|
||||
import type { ReactElement } from 'react';
|
||||
import { requestMeasure, requestMutation } from '../fasterdom/fasterdom';
|
||||
|
||||
import { DEBUG, DEBUG_MORE } from '../../config';
|
||||
import { throttleWithRafFallback } from '../../util/schedulers';
|
||||
import { throttleWith } from '../../util/schedulers';
|
||||
import { orderBy } from '../../util/iteratees';
|
||||
import { getUnequalProps } from '../../util/arePropsShallowEqual';
|
||||
import { handleError } from '../../util/handleError';
|
||||
import { incrementOverlayCounter } from '../../util/debugOverlay';
|
||||
import { isSignal } from '../../util/signals';
|
||||
import safeExec from '../../util/safeExec';
|
||||
|
||||
export type Props = AnyLiteral;
|
||||
export type FC<P extends Props = any> = (props: P) => any;
|
||||
@ -309,15 +311,26 @@ let pendingLayoutEffects = new Map<string, Effect>();
|
||||
let pendingLayoutCleanups = new Map<string, EffectCleanup>();
|
||||
let areImmediateEffectsPending = false;
|
||||
|
||||
const runUpdatePassOnRaf = throttleWithRafFallback(() => {
|
||||
/*
|
||||
Order:
|
||||
- component effect cleanups
|
||||
- component effects
|
||||
- measure tasks
|
||||
- mutation tasks
|
||||
- component updates
|
||||
- component layout effect cleanups
|
||||
- component layout effects
|
||||
- forced layout measure tasks
|
||||
- forced layout mutation tasks
|
||||
*/
|
||||
|
||||
const runUpdatePassOnRaf = throttleWith(requestMeasure, () => {
|
||||
areImmediateEffectsPending = true;
|
||||
|
||||
idsToExcludeFromUpdate = new Set();
|
||||
|
||||
const instancesToUpdate = Array
|
||||
.from(instancesPendingUpdate)
|
||||
.sort((a, b) => a.id - b.id);
|
||||
|
||||
instancesPendingUpdate = new Set();
|
||||
|
||||
const currentCleanups = pendingCleanups;
|
||||
@ -328,18 +341,19 @@ const runUpdatePassOnRaf = throttleWithRafFallback(() => {
|
||||
pendingEffects = new Map();
|
||||
currentEffects.forEach((cb) => cb());
|
||||
|
||||
instancesToUpdate.forEach(prepareComponentForFrame);
|
||||
requestMutation(() => {
|
||||
instancesToUpdate.forEach(prepareComponentForFrame);
|
||||
instancesToUpdate.forEach((instance) => {
|
||||
if (idsToExcludeFromUpdate!.has(instance.id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
instancesToUpdate.forEach((instance) => {
|
||||
if (idsToExcludeFromUpdate!.has(instance.id)) {
|
||||
return;
|
||||
}
|
||||
forceUpdateComponent(instance);
|
||||
});
|
||||
|
||||
forceUpdateComponent(instance);
|
||||
areImmediateEffectsPending = false;
|
||||
runImmediateEffects();
|
||||
});
|
||||
|
||||
areImmediateEffectsPending = false;
|
||||
runImmediateEffects();
|
||||
});
|
||||
|
||||
export function willRunImmediateEffects() {
|
||||
@ -356,18 +370,13 @@ export function runImmediateEffects() {
|
||||
currentLayoutEffects.forEach((cb) => cb());
|
||||
}
|
||||
|
||||
function scheduleUpdate(componentInstance: ComponentInstance) {
|
||||
instancesPendingUpdate.add(componentInstance);
|
||||
runUpdatePassOnRaf();
|
||||
}
|
||||
|
||||
export function renderComponent(componentInstance: ComponentInstance) {
|
||||
idsToExcludeFromUpdate.add(componentInstance.id);
|
||||
|
||||
const { Component, props } = componentInstance;
|
||||
let newRenderedValue;
|
||||
let newRenderedValue: any;
|
||||
|
||||
try {
|
||||
safeExec(() => {
|
||||
renderingInstance = componentInstance;
|
||||
componentInstance.hooks.state.cursor = 0;
|
||||
componentInstance.hooks.effects.cursor = 0;
|
||||
@ -414,13 +423,12 @@ export function renderComponent(componentInstance: ComponentInstance) {
|
||||
incrementOverlayCounter(`${componentName} duration`, duration);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
}, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[Teact] Error while rendering component ${componentInstance.name}`);
|
||||
handleError(err);
|
||||
|
||||
newRenderedValue = componentInstance.renderedValue;
|
||||
}
|
||||
});
|
||||
|
||||
if (componentInstance.isMounted && newRenderedValue === componentInstance.renderedValue) {
|
||||
return componentInstance.$element;
|
||||
@ -468,10 +476,8 @@ export function unmountComponent(componentInstance: ComponentInstance) {
|
||||
idsToExcludeFromUpdate.add(componentInstance.id);
|
||||
|
||||
componentInstance.hooks.effects.byCursor.forEach((effect) => {
|
||||
try {
|
||||
effect.cleanup?.();
|
||||
} catch (err: any) {
|
||||
handleError(err);
|
||||
if (effect.cleanup) {
|
||||
safeExec(effect.cleanup);
|
||||
}
|
||||
|
||||
effect.cleanup = undefined;
|
||||
@ -559,7 +565,8 @@ export function useState<T>(initial?: T, debugKey?: string): [T, StateHookSetter
|
||||
|
||||
byCursor[cursor].nextValue = newValue;
|
||||
|
||||
scheduleUpdate(componentInstance);
|
||||
instancesPendingUpdate.add(componentInstance);
|
||||
runUpdatePassOnRaf();
|
||||
|
||||
if (DEBUG_MORE) {
|
||||
if (componentInstance.name !== 'TeactNContainer') {
|
||||
@ -597,81 +604,75 @@ function useEffectBase(
|
||||
const { cursor, byCursor } = renderingInstance.hooks.effects;
|
||||
const componentInstance = renderingInstance;
|
||||
|
||||
function execCleanup() {
|
||||
const runEffectCleanup = () => safeExec(() => {
|
||||
const { cleanup } = byCursor[cursor];
|
||||
if (!cleanup) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let DEBUG_startAt: number | undefined;
|
||||
if (DEBUG) {
|
||||
DEBUG_startAt = performance.now();
|
||||
}
|
||||
|
||||
cleanup();
|
||||
|
||||
if (DEBUG) {
|
||||
const duration = performance.now() - DEBUG_startAt!;
|
||||
const componentName = componentInstance.name;
|
||||
if (duration > DEBUG_EFFECT_THRESHOLD) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[Teact] Slow cleanup at effect cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[Teact] Error in effect cleanup at cursor #${cursor} in ${componentInstance.name}`);
|
||||
handleError(err);
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let DEBUG_startAt: number | undefined;
|
||||
if (DEBUG) {
|
||||
DEBUG_startAt = performance.now();
|
||||
}
|
||||
|
||||
byCursor[cursor].cleanup = undefined;
|
||||
}
|
||||
cleanup();
|
||||
|
||||
function exec() {
|
||||
if (DEBUG) {
|
||||
const duration = performance.now() - DEBUG_startAt!;
|
||||
const componentName = componentInstance.name;
|
||||
if (duration > DEBUG_EFFECT_THRESHOLD) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(
|
||||
`[Teact] Slow cleanup at effect cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[Teact] Error in effect cleanup at cursor #${cursor} in ${componentInstance.name}`);
|
||||
}, () => {
|
||||
byCursor[cursor].cleanup = undefined;
|
||||
});
|
||||
|
||||
const runEffect = () => safeExec(() => {
|
||||
if (!componentInstance.isMounted) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let DEBUG_startAt: number | undefined;
|
||||
if (DEBUG) {
|
||||
DEBUG_startAt = performance.now();
|
||||
}
|
||||
|
||||
const result = effect();
|
||||
if (typeof result === 'function') {
|
||||
byCursor[cursor].cleanup = result;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
const duration = performance.now() - DEBUG_startAt!;
|
||||
const componentName = componentInstance.name;
|
||||
if (duration > DEBUG_EFFECT_THRESHOLD) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[Teact] Slow effect at cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`);
|
||||
}
|
||||
}
|
||||
} catch (err: any) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[Teact] Error in effect at cursor #${cursor} in ${componentInstance.name}`);
|
||||
handleError(err);
|
||||
// eslint-disable-next-line @typescript-eslint/naming-convention
|
||||
let DEBUG_startAt: number | undefined;
|
||||
if (DEBUG) {
|
||||
DEBUG_startAt = performance.now();
|
||||
}
|
||||
}
|
||||
|
||||
const result = effect();
|
||||
if (typeof result === 'function') {
|
||||
byCursor[cursor].cleanup = result;
|
||||
}
|
||||
|
||||
if (DEBUG) {
|
||||
const duration = performance.now() - DEBUG_startAt!;
|
||||
const componentName = componentInstance.name;
|
||||
if (duration > DEBUG_EFFECT_THRESHOLD) {
|
||||
// eslint-disable-next-line no-console
|
||||
console.warn(`[Teact] Slow effect at cursor #${cursor}: ${componentName}, ${Math.round(duration)} ms`);
|
||||
}
|
||||
}
|
||||
}, () => {
|
||||
// eslint-disable-next-line no-console
|
||||
console.error(`[Teact] Error in effect at cursor #${cursor} in ${componentInstance.name}`);
|
||||
});
|
||||
|
||||
function schedule() {
|
||||
const effectId = `${componentInstance.id}_${cursor}`;
|
||||
|
||||
if (isLayout) {
|
||||
pendingLayoutCleanups.set(effectId, execCleanup);
|
||||
pendingLayoutEffects.set(effectId, exec);
|
||||
pendingLayoutCleanups.set(effectId, runEffectCleanup);
|
||||
pendingLayoutEffects.set(effectId, runEffect);
|
||||
} else {
|
||||
pendingCleanups.set(effectId, execCleanup);
|
||||
pendingEffects.set(effectId, exec);
|
||||
pendingCleanups.set(effectId, runEffectCleanup);
|
||||
pendingEffects.set(effectId, runEffect);
|
||||
}
|
||||
|
||||
runUpdatePassOnRaf();
|
||||
|
||||
@ -1,11 +1,12 @@
|
||||
/* eslint-disable eslint-multitab-tt/set-global-only-variable */
|
||||
import type { FC, FC_withDebug, Props } from './teact';
|
||||
import React, { useEffect, useState } from './teact';
|
||||
import { requestMeasure } from '../fasterdom/fasterdom';
|
||||
|
||||
import { DEBUG, DEBUG_MORE } from '../../config';
|
||||
import useForceUpdate from '../../hooks/useForceUpdate';
|
||||
import generateIdFor from '../../util/generateIdFor';
|
||||
import { fastRafWithFallback, throttleWithTickEnd } from '../../util/schedulers';
|
||||
import { throttleWithTickEnd } from '../../util/schedulers';
|
||||
import arePropsShallowEqual, { getUnequalProps } from '../../util/arePropsShallowEqual';
|
||||
import { orderBy } from '../../util/iteratees';
|
||||
import { handleError } from '../../util/handleError';
|
||||
@ -76,7 +77,7 @@ function runCallbacks() {
|
||||
if (forceOnHeavyAnimation) {
|
||||
forceOnHeavyAnimation = false;
|
||||
} else if (isHeavyAnimating()) {
|
||||
fastRafWithFallback(runCallbacksThrottled);
|
||||
requestMeasure(runCallbacksThrottled);
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { fastRaf } from './schedulers';
|
||||
import type { Scheduler } from './schedulers';
|
||||
import { requestMeasure } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
interface AnimationInstance {
|
||||
isCancelled: boolean;
|
||||
@ -6,7 +7,7 @@ interface AnimationInstance {
|
||||
|
||||
let currentInstance: AnimationInstance | undefined;
|
||||
|
||||
export function animateSingle(tick: Function, instance?: AnimationInstance) {
|
||||
export function animateSingle(tick: Function, schedulerFn: Scheduler, instance?: AnimationInstance) {
|
||||
if (!instance) {
|
||||
if (currentInstance && !currentInstance.isCancelled) {
|
||||
currentInstance.isCancelled = true;
|
||||
@ -17,24 +18,24 @@ export function animateSingle(tick: Function, instance?: AnimationInstance) {
|
||||
}
|
||||
|
||||
if (!instance!.isCancelled && tick()) {
|
||||
fastRaf(() => {
|
||||
animateSingle(tick, instance);
|
||||
schedulerFn(() => {
|
||||
animateSingle(tick, schedulerFn, instance);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
export function animate(tick: Function) {
|
||||
fastRaf(() => {
|
||||
export function animate(tick: Function, schedulerFn: Scheduler) {
|
||||
schedulerFn(() => {
|
||||
if (tick()) {
|
||||
animate(tick);
|
||||
animate(tick, schedulerFn);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export function animateInstantly(tick: Function) {
|
||||
export function animateInstantly(tick: Function, schedulerFn: Scheduler) {
|
||||
if (tick()) {
|
||||
fastRaf(() => {
|
||||
animateInstantly(tick);
|
||||
schedulerFn(() => {
|
||||
animateInstantly(tick, schedulerFn);
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -94,7 +95,7 @@ export function animateNumber({
|
||||
}
|
||||
if (t === 1 && onEnd) onEnd();
|
||||
return t < 1;
|
||||
});
|
||||
}, requestMeasure);
|
||||
|
||||
return () => {
|
||||
canceled = true;
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { getActions, getGlobal } from '../global';
|
||||
import { requestNextMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import { AudioOrigin, GlobalSearchContent } from '../types';
|
||||
import type { ApiMessage } from '../api/types';
|
||||
@ -8,7 +9,6 @@ import safePlay from './safePlay';
|
||||
import { patchSafariProgressiveAudio, isSafariPatchInProgress } from './patchSafariProgressiveAudio';
|
||||
import type { MessageKey } from '../global/helpers';
|
||||
import { getMessageKey, parseMessageKey } from '../global/helpers';
|
||||
import { fastRaf } from './schedulers';
|
||||
import { selectCurrentMessageList, selectTabState } from '../global/selectors';
|
||||
|
||||
type Handler = (eventName: string, e: Event) => void;
|
||||
@ -178,11 +178,11 @@ export function register(
|
||||
|
||||
stop() {
|
||||
if (currentTrackId === trackId) {
|
||||
// Hack, reset src to remove default media session notification
|
||||
// Hack, reset `src` to remove default media session notification
|
||||
const prevSrc = audio.src;
|
||||
audio.pause();
|
||||
// onPause not called otherwise, but required to sync UI
|
||||
fastRaf(() => {
|
||||
// `onPause` not called otherwise, but required to sync UI
|
||||
requestNextMutation(() => {
|
||||
audio.src = '';
|
||||
audio.src = prevSrc;
|
||||
});
|
||||
|
||||
@ -2,6 +2,7 @@ import { IS_IOS } from './windowEnvironment';
|
||||
import { Lethargy } from './lethargy';
|
||||
import { clamp, round } from './math';
|
||||
import { debounce } from './schedulers';
|
||||
import windowSize from './windowSize';
|
||||
|
||||
export enum SwipeDirection {
|
||||
Up,
|
||||
@ -107,9 +108,10 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
|
||||
x: false,
|
||||
y: false,
|
||||
};
|
||||
const currentWindowSize = windowSize.get();
|
||||
let initialTouchCenter = {
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
x: currentWindowSize.width / 2,
|
||||
y: currentWindowSize.height / 2,
|
||||
};
|
||||
let initialSwipeAxis: TSwipeAxis | undefined;
|
||||
const minZoom = options.minZoom ?? 1;
|
||||
@ -217,9 +219,10 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
|
||||
x: false,
|
||||
y: false,
|
||||
};
|
||||
const newWindowSize = windowSize.get();
|
||||
initialTouchCenter = {
|
||||
x: window.innerWidth / 2,
|
||||
y: window.innerHeight / 2,
|
||||
x: newWindowSize.width / 2,
|
||||
y: newWindowSize.height / 2,
|
||||
};
|
||||
captureEvent = undefined;
|
||||
}
|
||||
@ -289,7 +292,7 @@ export function captureEvents(element: HTMLElement, options: CaptureOptions) {
|
||||
// Avoid conflicts with swipe-to-back gestures
|
||||
if (IS_IOS) {
|
||||
const x = (e as RealTouchEvent).touches[0].pageX;
|
||||
if (x <= IOS_SCREEN_EDGE_THRESHOLD || x >= window.innerWidth - IOS_SCREEN_EDGE_THRESHOLD) {
|
||||
if (x <= IOS_SCREEN_EDGE_THRESHOLD || x >= windowSize.get().width - IOS_SCREEN_EDGE_THRESHOLD) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,4 +1,6 @@
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
import EMOJI_REGEX, { removeVS16s } from '../lib/twemojiRegex';
|
||||
|
||||
import withCache from './withCache';
|
||||
|
||||
// Due to the fact that emoji from Apple do not contain some characters, it is necessary to remove them from emoji-data
|
||||
@ -32,8 +34,13 @@ function unifiedToNative(unified: string) {
|
||||
export const LOADED_EMOJIS = new Set<string>();
|
||||
|
||||
export function handleEmojiLoad(event: React.SyntheticEvent<HTMLImageElement>) {
|
||||
event.currentTarget.classList.add('open');
|
||||
const emoji = event.currentTarget;
|
||||
|
||||
LOADED_EMOJIS.add(event.currentTarget.dataset.path!);
|
||||
|
||||
requestMutation(() => {
|
||||
emoji.classList.add('open');
|
||||
});
|
||||
}
|
||||
|
||||
export function fixNonStandardEmoji(text: string) {
|
||||
|
||||
@ -4,13 +4,15 @@ import { FocusDirection } from '../types';
|
||||
|
||||
import {
|
||||
ANIMATION_LEVEL_MIN,
|
||||
FAST_SMOOTH_MAX_DISTANCE, FAST_SMOOTH_MAX_DURATION, FAST_SMOOTH_MIN_DURATION,
|
||||
FAST_SMOOTH_MIN_DURATION,
|
||||
FAST_SMOOTH_MAX_DURATION,
|
||||
FAST_SMOOTH_MAX_DISTANCE,
|
||||
FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE,
|
||||
} from '../config';
|
||||
import { IS_ANDROID } from './windowEnvironment';
|
||||
import { dispatchHeavyAnimationEvent } from '../hooks/useHeavyAnimationCheck';
|
||||
import { animateSingle } from './animation';
|
||||
import { fastRaf } from './schedulers';
|
||||
import { requestForcedReflow, requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
let isAnimating = false;
|
||||
|
||||
@ -23,6 +25,35 @@ export default function fastSmoothScroll(
|
||||
forceDirection?: FocusDirection,
|
||||
forceDuration?: number,
|
||||
forceNormalContainerHeight?: boolean,
|
||||
withForcedReflow = false,
|
||||
) {
|
||||
const args = [
|
||||
container,
|
||||
element,
|
||||
position,
|
||||
margin,
|
||||
maxDistance,
|
||||
forceDirection,
|
||||
forceDuration,
|
||||
forceNormalContainerHeight,
|
||||
] as const;
|
||||
|
||||
if (withForcedReflow) {
|
||||
requestForcedReflow(() => measure(...args));
|
||||
} else {
|
||||
requestMutation(measure(...args));
|
||||
}
|
||||
}
|
||||
|
||||
function measure(
|
||||
container: HTMLElement,
|
||||
element: HTMLElement,
|
||||
position: ScrollLogicalPosition | 'centerOrTop',
|
||||
margin = 0,
|
||||
maxDistance = FAST_SMOOTH_MAX_DISTANCE,
|
||||
forceDirection?: FocusDirection,
|
||||
forceDuration?: number,
|
||||
forceNormalContainerHeight?: boolean,
|
||||
) {
|
||||
if (
|
||||
forceDirection === FocusDirection.Static
|
||||
@ -71,29 +102,33 @@ export default function fastSmoothScroll(
|
||||
path = Math.min(path, remainingPath);
|
||||
}
|
||||
|
||||
if (path === 0) {
|
||||
return;
|
||||
}
|
||||
return () => {
|
||||
if (currentScrollTop !== scrollFrom) {
|
||||
container.scrollTop = scrollFrom;
|
||||
}
|
||||
|
||||
const target = scrollFrom + path;
|
||||
if (path === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (forceDuration === 0) {
|
||||
container.scrollTop = target;
|
||||
return;
|
||||
}
|
||||
const target = scrollFrom + path;
|
||||
|
||||
isAnimating = true;
|
||||
if (forceDuration === 0) {
|
||||
container.scrollTop = target;
|
||||
return;
|
||||
}
|
||||
|
||||
const absPath = Math.abs(path);
|
||||
const transition = absPath <= FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition;
|
||||
const duration = forceDuration || (
|
||||
FAST_SMOOTH_MIN_DURATION
|
||||
+ (absPath / FAST_SMOOTH_MAX_DISTANCE) * (FAST_SMOOTH_MAX_DURATION - FAST_SMOOTH_MIN_DURATION)
|
||||
);
|
||||
const startAt = Date.now();
|
||||
const onHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
||||
isAnimating = true;
|
||||
|
||||
const absPath = Math.abs(path);
|
||||
const transition = absPath <= FAST_SMOOTH_SHORT_TRANSITION_MAX_DISTANCE ? shortTransition : longTransition;
|
||||
const duration = forceDuration || (
|
||||
FAST_SMOOTH_MIN_DURATION
|
||||
+ (absPath / FAST_SMOOTH_MAX_DISTANCE) * (FAST_SMOOTH_MAX_DURATION - FAST_SMOOTH_MIN_DURATION)
|
||||
);
|
||||
const startAt = Date.now();
|
||||
const onHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
||||
|
||||
fastRaf(() => {
|
||||
animateSingle(() => {
|
||||
const t = Math.min((Date.now() - startAt) / duration, 1);
|
||||
const currentPath = path * (1 - transition(t));
|
||||
@ -107,8 +142,8 @@ export default function fastSmoothScroll(
|
||||
}
|
||||
|
||||
return isAnimating;
|
||||
});
|
||||
});
|
||||
}, requestMutation);
|
||||
};
|
||||
}
|
||||
|
||||
export function isAnimatingScroll() {
|
||||
@ -140,10 +175,10 @@ function calculateScrollFrom(
|
||||
return scrollTop;
|
||||
}
|
||||
|
||||
function longTransition(t: number) {
|
||||
return 1 - ((1 - t) ** 5);
|
||||
function shortTransition(t: number) {
|
||||
return 1 - ((1 - t) ** 3);
|
||||
}
|
||||
|
||||
function shortTransition(t: number) {
|
||||
return 1 - ((1 - t) ** 3.5);
|
||||
function longTransition(t: number) {
|
||||
return 1 - ((1 - t) ** 6.5);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { getGlobal } from '../global';
|
||||
|
||||
import { ANIMATION_LEVEL_MIN } from '../config';
|
||||
import { animate } from './animation';
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
const DEFAULT_DURATION = 300;
|
||||
|
||||
@ -12,10 +13,6 @@ export default function fastSmoothScrollHorizontal(container: HTMLElement, left:
|
||||
duration = 0;
|
||||
}
|
||||
|
||||
return scrollWithJs(container, left, duration);
|
||||
}
|
||||
|
||||
function scrollWithJs(container: HTMLElement, left: number, duration: number) {
|
||||
const isRtl = container.getAttribute('dir') === 'rtl';
|
||||
const {
|
||||
scrollLeft, offsetWidth: containerWidth, scrollWidth, dataset: { scrollId },
|
||||
@ -41,44 +38,44 @@ function scrollWithJs(container: HTMLElement, left: number, duration: number) {
|
||||
|
||||
const target = scrollLeft + path;
|
||||
|
||||
if (duration === 0) {
|
||||
container.scrollLeft = target;
|
||||
return Promise.resolve();
|
||||
}
|
||||
return new Promise<void>((resolve) => {
|
||||
requestMutation(() => {
|
||||
if (duration === 0) {
|
||||
container.scrollLeft = target;
|
||||
resolve();
|
||||
return;
|
||||
}
|
||||
|
||||
let isStopped = false;
|
||||
const id = Math.random().toString();
|
||||
container.dataset.scrollId = id;
|
||||
stopById.set(id, () => {
|
||||
isStopped = true;
|
||||
let isStopped = false;
|
||||
const id = Math.random().toString();
|
||||
container.dataset.scrollId = id;
|
||||
stopById.set(id, () => {
|
||||
isStopped = true;
|
||||
});
|
||||
|
||||
container.style.scrollSnapType = 'none';
|
||||
|
||||
const startAt = Date.now();
|
||||
|
||||
animate(() => {
|
||||
if (isStopped) return false;
|
||||
|
||||
const t = Math.min((Date.now() - startAt) / duration, 1);
|
||||
|
||||
const currentPath = path * (1 - transition(t));
|
||||
container.scrollLeft = Math.round(target - currentPath);
|
||||
|
||||
if (t >= 1) {
|
||||
container.style.scrollSnapType = '';
|
||||
delete container.dataset.scrollId;
|
||||
stopById.delete(id);
|
||||
resolve();
|
||||
}
|
||||
|
||||
return t < 1;
|
||||
}, requestMutation);
|
||||
});
|
||||
});
|
||||
|
||||
container.style.scrollSnapType = 'none';
|
||||
|
||||
let resolve: VoidFunction;
|
||||
const promise = new Promise<void>((r) => {
|
||||
resolve = r;
|
||||
});
|
||||
const startAt = Date.now();
|
||||
|
||||
animate(() => {
|
||||
if (isStopped) return false;
|
||||
|
||||
const t = Math.min((Date.now() - startAt) / duration, 1);
|
||||
|
||||
const currentPath = path * (1 - transition(t));
|
||||
container.scrollLeft = Math.round(target - currentPath);
|
||||
|
||||
if (t >= 1) {
|
||||
container.style.scrollSnapType = '';
|
||||
delete container.dataset.scrollId;
|
||||
stopById.delete(id);
|
||||
resolve();
|
||||
}
|
||||
return t < 1;
|
||||
});
|
||||
|
||||
return promise;
|
||||
}
|
||||
|
||||
function transition(t: number) {
|
||||
|
||||
@ -3,12 +3,12 @@ import { throttle } from './schedulers';
|
||||
import { getAllMultitabTokens } from './establishMultitabRole';
|
||||
import { IS_MULTITAB_SUPPORTED } from './windowEnvironment';
|
||||
|
||||
window.addEventListener('error', handleErrorEvent);
|
||||
window.addEventListener('unhandledrejection', handleErrorEvent);
|
||||
|
||||
// eslint-disable-next-line prefer-destructuring
|
||||
const APP_ENV = process.env.APP_ENV;
|
||||
|
||||
window.addEventListener('error', handleErrorEvent);
|
||||
window.addEventListener('unhandledrejection', handleErrorEvent);
|
||||
|
||||
function handleErrorEvent(e: ErrorEvent | PromiseRejectionEvent) {
|
||||
// https://stackoverflow.com/questions/49384120/resizeobserver-loop-limit-exceeded
|
||||
if (e instanceof ErrorEvent && e.message === 'ResizeObserver loop limit exceeded') {
|
||||
|
||||
20
src/util/safeExec.ts
Normal file
20
src/util/safeExec.ts
Normal file
@ -0,0 +1,20 @@
|
||||
import { DEBUG_MORE } from '../config';
|
||||
import { handleError } from './handleError';
|
||||
|
||||
const SAFE_EXEC_ENABLED = !DEBUG_MORE;
|
||||
|
||||
export default function safeExec(cb: Function, rescue?: (err: Error) => void, always?: NoneToVoidFunction) {
|
||||
if (!SAFE_EXEC_ENABLED) {
|
||||
return cb();
|
||||
}
|
||||
|
||||
try {
|
||||
return cb();
|
||||
} catch (err: any) {
|
||||
rescue?.(err);
|
||||
handleError(err);
|
||||
return undefined;
|
||||
} finally {
|
||||
always?.();
|
||||
}
|
||||
}
|
||||
@ -1,6 +1,4 @@
|
||||
type Scheduler =
|
||||
typeof requestAnimationFrame
|
||||
| typeof onTickEnd;
|
||||
export type Scheduler = typeof requestAnimationFrame | typeof onTickEnd;
|
||||
|
||||
export function debounce<F extends AnyToVoidFunction>(
|
||||
fn: F,
|
||||
@ -64,18 +62,6 @@ export function throttle<F extends AnyToVoidFunction>(
|
||||
};
|
||||
}
|
||||
|
||||
export function fastRafWithFallback<F extends AnyToVoidFunction>(fn: F) {
|
||||
return fastRaf(fn, true);
|
||||
}
|
||||
|
||||
export function throttleWithRafFallback<F extends AnyToVoidFunction>(fn: F) {
|
||||
return throttleWith(fastRafWithFallback, fn);
|
||||
}
|
||||
|
||||
export function throttleWithRaf<F extends AnyToVoidFunction>(fn: F) {
|
||||
return throttleWith(fastRaf, fn);
|
||||
}
|
||||
|
||||
export function throttleWithTickEnd<F extends AnyToVoidFunction>(fn: F) {
|
||||
return throttleWith(onTickEnd, fn);
|
||||
}
|
||||
|
||||
@ -1,9 +1,11 @@
|
||||
import { requestMutation } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
import type { ISettings } from '../types';
|
||||
|
||||
import { animate } from './animation';
|
||||
import { lerp } from './math';
|
||||
|
||||
import themeColors from '../styles/themes.json';
|
||||
import { lerp } from './math';
|
||||
|
||||
type RGBAColor = {
|
||||
r: number;
|
||||
@ -39,32 +41,36 @@ const switchTheme = (theme: ISettings['theme'], withAnimation: boolean) => {
|
||||
const startAt = Date.now();
|
||||
const themeColorTag = document.querySelector('meta[name="theme-color"]');
|
||||
|
||||
document.documentElement.classList.remove(`theme-${isDarkTheme ? 'light' : 'dark'}`);
|
||||
if (isInitialized) {
|
||||
document.documentElement.classList.add('no-animations');
|
||||
}
|
||||
document.documentElement.classList.add(themeClassName);
|
||||
if (themeColorTag) {
|
||||
themeColorTag.setAttribute('content', isDarkTheme ? '#212121' : '#fff');
|
||||
}
|
||||
requestMutation(() => {
|
||||
document.documentElement.classList.remove(`theme-${isDarkTheme ? 'light' : 'dark'}`);
|
||||
if (isInitialized) {
|
||||
document.documentElement.classList.add('no-animations');
|
||||
}
|
||||
document.documentElement.classList.add(themeClassName);
|
||||
if (themeColorTag) {
|
||||
themeColorTag.setAttribute('content', isDarkTheme ? '#212121' : '#fff');
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
document.documentElement.classList.remove('no-animations');
|
||||
}, ENABLE_ANIMATION_DELAY_MS);
|
||||
setTimeout(() => {
|
||||
requestMutation(() => {
|
||||
document.documentElement.classList.remove('no-animations');
|
||||
});
|
||||
}, ENABLE_ANIMATION_DELAY_MS);
|
||||
|
||||
isInitialized = true;
|
||||
isInitialized = true;
|
||||
|
||||
if (shouldAnimate) {
|
||||
animate(() => {
|
||||
const t = Math.min((Date.now() - startAt) / DURATION_MS, 1);
|
||||
if (shouldAnimate) {
|
||||
animate(() => {
|
||||
const t = Math.min((Date.now() - startAt) / DURATION_MS, 1);
|
||||
|
||||
applyColorAnimationStep(startIndex, endIndex, transition(t));
|
||||
applyColorAnimationStep(startIndex, endIndex, transition(t));
|
||||
|
||||
return t < 1;
|
||||
});
|
||||
} else {
|
||||
applyColorAnimationStep(startIndex, endIndex);
|
||||
}
|
||||
return t < 1;
|
||||
}, requestMutation);
|
||||
} else {
|
||||
applyColorAnimationStep(startIndex, endIndex);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
function transition(t: number) {
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { IOpusRecorder } from 'opus-recorder';
|
||||
import { requestMeasure } from '../lib/fasterdom/fasterdom';
|
||||
|
||||
export type Result = { blob: Blob; duration: number; waveform: number[] };
|
||||
|
||||
@ -96,7 +97,7 @@ function subscribeToAnalyzer(recorder: IOpusRecorder, cb: Function) {
|
||||
|
||||
cb(volume < MIN_VOLUME ? 0 : volume);
|
||||
|
||||
requestAnimationFrame(tick);
|
||||
requestMeasure(tick);
|
||||
}
|
||||
|
||||
tick();
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user