UI Rendering: Fix flicks (#6352)
This commit is contained in:
parent
ff0d9edc73
commit
89e37c280b
@ -97,6 +97,7 @@ type OwnProps = {
|
||||
observeIntersection?: ObserveFn;
|
||||
onDragEnter?: (chatId: string) => void;
|
||||
withTags?: boolean;
|
||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -172,6 +173,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
chatFoldersById,
|
||||
areTagsEnabled,
|
||||
withTags,
|
||||
onReorderAnimationEnd,
|
||||
}) => {
|
||||
const {
|
||||
openChat,
|
||||
@ -234,6 +236,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
||||
orderDiff,
|
||||
isSavedDialog,
|
||||
isPreview,
|
||||
onReorderAnimationEnd,
|
||||
topics,
|
||||
noForumTitle: shouldRenderTags,
|
||||
});
|
||||
|
||||
@ -105,7 +105,7 @@ const ChatList: FC<OwnProps> = ({
|
||||
? archiveSettings?.isMinimized ? ARCHIVE_MINIMIZED_HEIGHT : CHAT_HEIGHT_PX : 0;
|
||||
const frozenNotificationHeight = shouldShowFrozenAccountNotification ? 68 : 0;
|
||||
|
||||
const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds);
|
||||
const { orderDiffById, getAnimationType, onReorderAnimationEnd: onReorderAnimationEnd } = useOrderDiff(orderedIds);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(undefined, orderedIds, undefined, CHAT_LIST_SLICE);
|
||||
|
||||
@ -242,6 +242,7 @@ const ChatList: FC<OwnProps> = ({
|
||||
isSavedDialog={isSaved}
|
||||
animationType={getAnimationType(id)}
|
||||
orderDiff={orderDiffById[id]}
|
||||
onReorderAnimationEnd={onReorderAnimationEnd}
|
||||
offsetTop={offsetTop}
|
||||
observeIntersection={observe}
|
||||
onDragEnter={handleDragEnter}
|
||||
|
||||
@ -130,7 +130,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
: [];
|
||||
}, [topicsInfo]);
|
||||
|
||||
const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds, chat?.id);
|
||||
const { orderDiffById, getAnimationType, onReorderAnimationEnd } = useOrderDiff(orderedIds, chat?.id);
|
||||
|
||||
const [viewportIds, getMore] = useInfiniteScroll(() => {
|
||||
if (!chat) return;
|
||||
@ -207,6 +207,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
observeIntersection={observe}
|
||||
animationType={getAnimationType(id)}
|
||||
orderDiff={orderDiffById[id]}
|
||||
onReorderAnimationEnd={onReorderAnimationEnd}
|
||||
/>
|
||||
));
|
||||
}
|
||||
|
||||
@ -57,6 +57,7 @@ type OwnProps = {
|
||||
observeIntersection?: ObserveFn;
|
||||
orderDiff: number;
|
||||
animationType: ChatAnimationTypes;
|
||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -96,6 +97,7 @@ const Topic: FC<OwnProps & StateProps> = ({
|
||||
draft,
|
||||
wasTopicOpened,
|
||||
topics,
|
||||
onReorderAnimationEnd,
|
||||
}) => {
|
||||
const {
|
||||
openThread,
|
||||
@ -152,6 +154,7 @@ const Topic: FC<OwnProps & StateProps> = ({
|
||||
animationType,
|
||||
withInterfaceAnimations,
|
||||
orderDiff,
|
||||
onReorderAnimationEnd,
|
||||
});
|
||||
|
||||
const handleOpenTopic = useLastCallback((e: React.MouseEvent) => {
|
||||
|
||||
@ -8,7 +8,7 @@ import type {
|
||||
} from '../../../../api/types';
|
||||
import type { ObserveFn } from '../../../../hooks/useIntersectionObserver';
|
||||
|
||||
import { ANIMATION_END_DELAY, CHAT_HEIGHT_PX } from '../../../../config';
|
||||
import { CHAT_HEIGHT_PX } from '../../../../config';
|
||||
import { requestMutation } from '../../../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getMessageIsSpoiler,
|
||||
@ -17,6 +17,7 @@ import {
|
||||
getMessageVideo,
|
||||
} from '../../../../global/helpers';
|
||||
import { getMessageSenderName } from '../../../../global/helpers/peers';
|
||||
import { waitStartingTransitionsEnd } from '../../../../util/animations/waitTransitionEnd';
|
||||
import buildClassName from '../../../../util/buildClassName';
|
||||
import renderText from '../../../common/helpers/renderText';
|
||||
import { renderTextWithEntities } from '../../../common/helpers/renderTextWithEntities';
|
||||
@ -33,8 +34,6 @@ import Icon from '../../../common/icons/Icon';
|
||||
import MessageSummary from '../../../common/MessageSummary';
|
||||
import TypingStatus from '../../../common/TypingStatus';
|
||||
|
||||
const ANIMATION_DURATION = 200;
|
||||
|
||||
export default function useChatListEntry({
|
||||
chat,
|
||||
topics,
|
||||
@ -53,6 +52,7 @@ export default function useChatListEntry({
|
||||
isSavedDialog,
|
||||
isPreview,
|
||||
noForumTitle,
|
||||
onReorderAnimationEnd,
|
||||
}: {
|
||||
chat?: ApiChat;
|
||||
topics?: Record<number, ApiTopic>;
|
||||
@ -72,6 +72,7 @@ export default function useChatListEntry({
|
||||
orderDiff: number;
|
||||
withInterfaceAnimations?: boolean;
|
||||
noForumTitle?: boolean;
|
||||
onReorderAnimationEnd?: NoneToVoidFunction;
|
||||
}) {
|
||||
const lang = useLang();
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
@ -171,6 +172,18 @@ export default function useChatListEntry({
|
||||
return;
|
||||
}
|
||||
|
||||
let isCancelled = false;
|
||||
|
||||
const notifyAnimationEnd = () => {
|
||||
if (isCancelled) return;
|
||||
requestMutation(() => {
|
||||
element.classList.remove('animate-opacity', 'animate-transform');
|
||||
element.style.opacity = '';
|
||||
element.style.transform = '';
|
||||
});
|
||||
onReorderAnimationEnd?.();
|
||||
};
|
||||
|
||||
// TODO Refactor animation: create `useListAnimation` that owns `orderDiff` and `animationType`
|
||||
if (animationType === ChatAnimationTypes.Opacity) {
|
||||
element.style.opacity = '0';
|
||||
@ -178,6 +191,8 @@ export default function useChatListEntry({
|
||||
requestMutation(() => {
|
||||
element.classList.add('animate-opacity');
|
||||
element.style.opacity = '1';
|
||||
|
||||
waitStartingTransitionsEnd(element).then(notifyAnimationEnd);
|
||||
});
|
||||
} else if (animationType === ChatAnimationTypes.Move) {
|
||||
element.style.transform = `translate3d(0, ${-orderDiff * CHAT_HEIGHT_PX}px, 0)`;
|
||||
@ -185,19 +200,17 @@ export default function useChatListEntry({
|
||||
requestMutation(() => {
|
||||
element.classList.add('animate-transform');
|
||||
element.style.transform = '';
|
||||
|
||||
waitStartingTransitionsEnd(element).then(notifyAnimationEnd);
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
|
||||
setTimeout(() => {
|
||||
requestMutation(() => {
|
||||
element.classList.remove('animate-opacity', 'animate-transform');
|
||||
element.style.opacity = '';
|
||||
element.style.transform = '';
|
||||
});
|
||||
}, ANIMATION_DURATION + ANIMATION_END_DELAY);
|
||||
}, [withInterfaceAnimations, orderDiff, animationType]);
|
||||
return () => {
|
||||
isCancelled = true;
|
||||
};
|
||||
}, [withInterfaceAnimations, orderDiff, animationType, onReorderAnimationEnd]);
|
||||
|
||||
return {
|
||||
renderSubtitle,
|
||||
|
||||
@ -1,9 +1,14 @@
|
||||
import { useMemo } from '../../../../lib/teact/teact';
|
||||
import { useMemo, useRef } from '../../../../lib/teact/teact';
|
||||
|
||||
import { mapValues } from '../../../../util/iteratees';
|
||||
import { useChatAnimationType } from './useChatAnimationType';
|
||||
|
||||
import useForceUpdate from '../../../../hooks/useForceUpdate';
|
||||
import useLastCallback from '../../../../hooks/useLastCallback';
|
||||
import usePreviousDeprecated from '../../../../hooks/usePreviousDeprecated';
|
||||
import useSyncEffect from '../../../../hooks/useSyncEffect';
|
||||
|
||||
const EMPTY_ORDER_DIFF = {};
|
||||
|
||||
export default function useOrderDiff(orderedIds: (string | number)[] | undefined, key?: string) {
|
||||
const orderById = useMemo(() => {
|
||||
@ -20,20 +25,35 @@ export default function useOrderDiff(orderedIds: (string | number)[] | undefined
|
||||
const prevOrderById = usePreviousDeprecated(orderById);
|
||||
const prevChatId = usePreviousDeprecated(key);
|
||||
|
||||
const orderDiffById = useMemo(() => {
|
||||
const orderDiffByIdRef = useRef<Record<string | number, number>>(EMPTY_ORDER_DIFF);
|
||||
const forceUpdate = useForceUpdate();
|
||||
|
||||
const onReorderAnimationEnd = useLastCallback(() => {
|
||||
if (orderDiffByIdRef.current === EMPTY_ORDER_DIFF) return;
|
||||
|
||||
orderDiffByIdRef.current = EMPTY_ORDER_DIFF;
|
||||
forceUpdate();
|
||||
});
|
||||
|
||||
useSyncEffect(() => {
|
||||
if (!orderById || !prevOrderById || key !== prevChatId) {
|
||||
return {};
|
||||
orderDiffByIdRef.current = EMPTY_ORDER_DIFF;
|
||||
return;
|
||||
}
|
||||
|
||||
return mapValues(orderById, (order, id) => {
|
||||
const diff = mapValues(orderById, (order, id) => {
|
||||
return prevOrderById[id] !== undefined ? order - prevOrderById[id] : -Infinity;
|
||||
});
|
||||
|
||||
const hasChanges = Object.values(diff).some((value) => value !== 0);
|
||||
orderDiffByIdRef.current = hasChanges ? diff : EMPTY_ORDER_DIFF;
|
||||
}, [key, orderById, prevChatId, prevOrderById]);
|
||||
|
||||
const getAnimationType = useChatAnimationType(orderDiffById);
|
||||
const getAnimationType = useChatAnimationType(orderDiffByIdRef.current);
|
||||
|
||||
return {
|
||||
orderDiffById,
|
||||
orderDiffById: orderDiffByIdRef.current,
|
||||
getAnimationType,
|
||||
onReorderAnimationEnd,
|
||||
};
|
||||
}
|
||||
|
||||
@ -23,7 +23,7 @@
|
||||
|
||||
background: var(--background-color);
|
||||
|
||||
transition: background-color 0.15s, color 0.15s;
|
||||
transition: background-color 0.15s, color 0.15s, opacity 150ms, backdrop-filter 150ms, filter 150ms;
|
||||
|
||||
.label {
|
||||
overflow: hidden;
|
||||
@ -70,8 +70,6 @@
|
||||
opacity: 0;
|
||||
background-color: var(--pattern-color);
|
||||
|
||||
transition: opacity 150ms, backdrop-filter 150ms, filter 150ms;
|
||||
|
||||
&::after {
|
||||
content: attr(data-cnt);
|
||||
|
||||
@ -96,6 +94,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: var(--pattern-color);
|
||||
}
|
||||
|
||||
.Message:hover &, &.loading {
|
||||
opacity: 1;
|
||||
}
|
||||
@ -139,6 +141,10 @@
|
||||
filter: none;
|
||||
backdrop-filter: none;
|
||||
}
|
||||
|
||||
&.disabled {
|
||||
background-color: rgba(255, 255, 255, 0.08);
|
||||
}
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@ -249,6 +255,8 @@
|
||||
&.disabled {
|
||||
pointer-events: none;
|
||||
cursor: var(--custom-cursor, default);
|
||||
opacity: 0.65;
|
||||
background-color: var(--hover-color);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -21,7 +21,7 @@ import Spinner from '../../ui/Spinner';
|
||||
import './CommentButton.scss';
|
||||
|
||||
type OwnProps = {
|
||||
threadInfo: ApiCommentsInfo;
|
||||
threadInfo?: ApiCommentsInfo;
|
||||
disabled?: boolean;
|
||||
isLoading?: boolean;
|
||||
isCustomShape?: boolean;
|
||||
@ -45,10 +45,15 @@ const CommentButton: FC<OwnProps> = ({
|
||||
const lang = useLang();
|
||||
const {
|
||||
originMessageId, chatId, messagesCount, lastMessageId, lastReadInboxMessageId, recentReplierIds, originChannelId,
|
||||
} = threadInfo;
|
||||
} = threadInfo || {};
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
const global = getGlobal();
|
||||
|
||||
if (!originMessageId || !originChannelId) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (selectIsCurrentUserFrozen(global)) {
|
||||
openFrozenAccountModal();
|
||||
return;
|
||||
@ -71,10 +76,6 @@ const CommentButton: FC<OwnProps> = ({
|
||||
}).filter(Boolean);
|
||||
}, [recentReplierIds]);
|
||||
|
||||
if (messagesCount === undefined) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function renderRecentRepliers() {
|
||||
return (
|
||||
Boolean(recentRepliers?.length) && (
|
||||
@ -102,7 +103,7 @@ const CommentButton: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
data-cnt={formatIntegerCompact(lang, messagesCount)}
|
||||
data-cnt={formatIntegerCompact(lang, messagesCount || 0)}
|
||||
className={buildClassName(
|
||||
'CommentButton',
|
||||
hasUnread && 'has-unread',
|
||||
|
||||
@ -787,11 +787,14 @@ const Message = ({
|
||||
|
||||
const phoneCall = action?.type === 'phoneCall' ? action : undefined;
|
||||
|
||||
const isMediaWithCommentButton = (repliesThreadInfo || (hasLinkedChat && isChannel && isLocal))
|
||||
const commentsThreadInfo = repliesThreadInfo?.isCommentsInfo ? repliesThreadInfo : undefined;
|
||||
const isLocalWithCommentButton = hasLinkedChat && isChannel && isLocal;
|
||||
|
||||
const isMediaWithCommentButton = (commentsThreadInfo || isLocalWithCommentButton)
|
||||
&& !isInDocumentGroupNotLast
|
||||
&& messageListType === 'thread'
|
||||
&& !noComments;
|
||||
const withCommentButton = repliesThreadInfo?.isCommentsInfo
|
||||
const withCommentButton = (commentsThreadInfo || isLocalWithCommentButton)
|
||||
&& !isInDocumentGroupNotLast && messageListType === 'thread'
|
||||
&& !noComments;
|
||||
const withQuickReactionButton = !isTouchScreen && !phoneCall && !isInSelectMode && defaultReaction
|
||||
@ -1726,8 +1729,8 @@ const Message = ({
|
||||
>
|
||||
{withCommentButton && isCustomShape && (
|
||||
<CommentButton
|
||||
threadInfo={repliesThreadInfo}
|
||||
disabled={noComments}
|
||||
threadInfo={commentsThreadInfo}
|
||||
disabled={noComments || !commentsThreadInfo}
|
||||
isLoading={isLoadingComments}
|
||||
isCustomShape
|
||||
asActionButton
|
||||
@ -1761,8 +1764,8 @@ const Message = ({
|
||||
)}
|
||||
{withCommentButton && !isCustomShape && (
|
||||
<CommentButton
|
||||
threadInfo={repliesThreadInfo}
|
||||
disabled={noComments}
|
||||
threadInfo={commentsThreadInfo}
|
||||
disabled={noComments || !commentsThreadInfo}
|
||||
isLoading={isLoadingComments}
|
||||
/>
|
||||
)}
|
||||
|
||||
@ -179,6 +179,14 @@ const Photo = <T,>({
|
||||
isOpen: !fullMediaData && !isLoadAllowed,
|
||||
withShouldRender: true,
|
||||
});
|
||||
const {
|
||||
ref: transferProgressRef,
|
||||
shouldRender: shouldRenderTransferProgress,
|
||||
} = useShowTransition({
|
||||
isOpen: isTransferring,
|
||||
noMountTransition: wasLoadDisabled,
|
||||
withShouldRender: true,
|
||||
});
|
||||
|
||||
const handleClick = useLastCallback((e: React.MouseEvent<HTMLElement>) => {
|
||||
if (isUploading) {
|
||||
@ -291,10 +299,9 @@ const Photo = <T,>({
|
||||
className="media-spoiler"
|
||||
isNsfw={isMediaNsfw}
|
||||
/>
|
||||
{isTransferring && (
|
||||
<span className="message-transfer-progress">
|
||||
{Math.round(transferProgress * 100)}
|
||||
%
|
||||
{shouldRenderTransferProgress && (
|
||||
<span ref={transferProgressRef} className="message-transfer-progress">
|
||||
{`${Math.round(transferProgress * 100)}%`}
|
||||
</span>
|
||||
)}
|
||||
<SensitiveContentConfirmModal
|
||||
|
||||
@ -188,6 +188,14 @@ const Video = <T,>({
|
||||
} = useShowTransition({
|
||||
isOpen: Boolean((isLoadAllowed || fullMediaData) && !isPlayAllowed && !shouldRenderSpinner),
|
||||
});
|
||||
const {
|
||||
ref: transferProgressRef,
|
||||
shouldRender: shouldRenderTransferProgress,
|
||||
} = useShowTransition({
|
||||
isOpen: isTransferring && (!isUnsupported || isDownloading),
|
||||
noMountTransition: wasLoadDisabled,
|
||||
withShouldRender: true,
|
||||
});
|
||||
|
||||
const [playProgress, setPlayProgress] = useState<number>(0);
|
||||
const handleTimeUpdate = useLastCallback((e: React.SyntheticEvent<HTMLVideoElement>) => {
|
||||
@ -324,8 +332,8 @@ const Video = <T,>({
|
||||
{!isLoadAllowed && !fullMediaData && (
|
||||
<Icon name="download" />
|
||||
)}
|
||||
{isTransferring && (!isUnsupported || isDownloading) ? (
|
||||
<span className="message-transfer-progress">
|
||||
{shouldRenderTransferProgress ? (
|
||||
<span ref={transferProgressRef} className="message-transfer-progress">
|
||||
{(isUploading || isDownloading) ? `${Math.round(transferProgress * 100)}%` : '...'}
|
||||
</span>
|
||||
) : (
|
||||
|
||||
@ -1,5 +1,6 @@
|
||||
import { addCallback } from '../../../lib/teact/teactn';
|
||||
|
||||
import type { ApiThreadInfo } from '../../../api/types/messages';
|
||||
import type { Thread, ThreadId } from '../../../types';
|
||||
import type { RequiredGlobalActions } from '../../index';
|
||||
import type { ActionReturnType, GlobalState } from '../../types';
|
||||
@ -128,6 +129,19 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
selectChatMessage(global, global.currentUserId!, Number(messageId))
|
||||
)).filter(Boolean);
|
||||
|
||||
// Memoize thread infos for last messages
|
||||
const lastMessagesThreadInfos: { chatId: string; messageId: number; threadInfo: ApiThreadInfo }[] = [];
|
||||
lastMessages.forEach((message) => {
|
||||
const threadInfo = selectThreadInfo(global, message.chatId, message.id);
|
||||
if (threadInfo) {
|
||||
lastMessagesThreadInfos.push({
|
||||
chatId: message.chatId,
|
||||
messageId: message.id,
|
||||
threadInfo,
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
for (const { id: tabId } of Object.values(global.byTabId)) {
|
||||
global = getGlobal();
|
||||
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global, tabId) || {};
|
||||
@ -256,6 +270,14 @@ async function loadAndReplaceMessages<T extends GlobalState>(global: T, actions:
|
||||
});
|
||||
});
|
||||
|
||||
// Restore thread infos
|
||||
lastMessagesThreadInfos.forEach(({ chatId, messageId, threadInfo }) => {
|
||||
const memoThreadInfo = selectThreadInfo(global, chatId, messageId);
|
||||
if (!memoThreadInfo) {
|
||||
global = updateThreadInfo(global, chatId, String(messageId), threadInfo);
|
||||
}
|
||||
});
|
||||
|
||||
// Restore last messages
|
||||
global = addMessages(global, lastMessages);
|
||||
global = addMessages(global, savedLastMessages);
|
||||
|
||||
@ -587,6 +587,9 @@ addActionHandler('apiUpdate', (global, actions, update): ActionReturnType => {
|
||||
} = update;
|
||||
|
||||
global = updateThreadInfos(global, [threadInfo]);
|
||||
setGlobal(global);
|
||||
global = getGlobal();
|
||||
|
||||
const { chatId, threadId } = threadInfo;
|
||||
if (!chatId || !threadId) return;
|
||||
|
||||
|
||||
19
src/util/animations/waitTransitionEnd.ts
Normal file
19
src/util/animations/waitTransitionEnd.ts
Normal file
@ -0,0 +1,19 @@
|
||||
import { fastRaf } from '../schedulers';
|
||||
|
||||
export type Scheduler = typeof requestAnimationFrame | typeof fastRaf;
|
||||
|
||||
export function waitCurrentTransitionsEnd(element: HTMLElement) {
|
||||
return Promise.allSettled(
|
||||
element.getAnimations({ subtree: true })
|
||||
.filter((a) => a instanceof CSSTransition)
|
||||
.map((a) => a.finished),
|
||||
);
|
||||
}
|
||||
|
||||
export function waitStartingTransitionsEnd(element: HTMLElement, scheduler: Scheduler = fastRaf) {
|
||||
return new Promise<void>((resolve) => {
|
||||
scheduler(() => {
|
||||
waitCurrentTransitionsEnd(element).then(() => resolve());
|
||||
});
|
||||
});
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user