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