diff --git a/src/api/types/updates.ts b/src/api/types/updates.ts index 0bcd1ea57..bd1d14178 100644 --- a/src/api/types/updates.ts +++ b/src/api/types/updates.ts @@ -638,8 +638,7 @@ export type ApiUpdate = ( ApiUpdatePhoneCall | ApiUpdatePhoneCallSignalingData | ApiUpdatePhoneCallMediaState | ApiUpdatePhoneCallConnectionState | ApiUpdateBotMenuButton | ApiUpdateTranscribedAudio | ApiUpdateUserEmojiStatus | ApiUpdateMessageExtendedMedia | ApiUpdateConfig | ApiUpdateTopicNotifyExceptions | ApiUpdatePinnedTopic | - ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | - ApiRequestInitApi + ApiUpdatePinnedTopicsOrder | ApiUpdateTopic | ApiUpdateTopics | ApiUpdateRecentEmojiStatuses | ApiRequestInitApi ); export type OnApiUpdate = (update: ApiUpdate) => void; diff --git a/src/components/ui/Transition.tsx b/src/components/ui/Transition.tsx index 3056727e0..dde5bff05 100644 --- a/src/components/ui/Transition.tsx +++ b/src/components/ui/Transition.tsx @@ -1,7 +1,9 @@ import type { RefObject } from 'react'; import type { FC } from '../../lib/teact/teact'; import React, { useLayoutEffect, useRef } from '../../lib/teact/teact'; +import { addExtraClass, removeExtraClass, toggleExtraClass } from '../../lib/teact/teact-dom'; import { getGlobal } from '../../global'; + import type { GlobalState } from '../../global/types'; import { ANIMATION_LEVEL_MIN } from '../../config'; @@ -36,10 +38,10 @@ export type TransitionProps = { children: React.ReactNode | ChildrenFn; }; -const classNames = { +const FALLBACK_ANIMATION_END = 1000; +const CLASSES = { active: 'Transition__slide--active', }; -const FALLBACK_ANIMATION_END = 1000; const Transition: FC = ({ ref, @@ -92,21 +94,21 @@ const Transition: FC = ({ } const container = containerRef.current!; - const childElements = container.children; + if (childElements.length === 1 && !activeKeyChanged) { + const firstChild = childElements[0] as HTMLElement; if (name.startsWith('slide-optimized')) { - (childElements[0] as HTMLElement).style.transition = 'none'; - (childElements[0] as HTMLElement).style.transform = 'translate3d(0, 0, 0)'; + firstChild.style.transition = 'none'; + firstChild.style.transform = 'translate3d(0, 0, 0)'; } - childElements[0].classList.add(classNames.active); + addExtraClass(firstChild, CLASSES.active); return; } const childNodes = Array.from(container.childNodes); - if (!activeKeyChanged || !childNodes.length) { return; } @@ -142,14 +144,16 @@ const Transition: FC = ({ return; } - container.classList.remove('animating'); - container.classList.toggle('backwards', isBackwards); + removeExtraClass(container, 'animating'); + toggleExtraClass(container, 'backwards', isBackwards); if (name === 'none' || animationLevel === ANIMATION_LEVEL_MIN) { childNodes.forEach((node, i) => { if (node instanceof HTMLElement) { - node.classList.remove('from', 'through', 'to'); - node.classList.toggle(classNames.active, i === activeIndex); + removeExtraClass(node, 'from'); + removeExtraClass(node, 'through'); + removeExtraClass(node, 'to'); + toggleExtraClass(node, CLASSES.active, i === activeIndex); } }); @@ -160,19 +164,19 @@ const Transition: FC = ({ childNodes.forEach((node, i) => { if (node instanceof HTMLElement) { - node.classList.remove(classNames.active); - node.classList.toggle('from', i === prevActiveIndex); - node.classList.toggle('through', ( - (i > prevActiveIndex && i < activeIndex) || (i < prevActiveIndex && i > activeIndex) - )); - node.classList.toggle('to', i === activeIndex); + removeExtraClass(node, CLASSES.active); + + toggleExtraClass(node, 'from', i === prevActiveIndex); + const isThrough = (i > prevActiveIndex && i < activeIndex) || (i < prevActiveIndex && i > activeIndex); + toggleExtraClass(node, 'through', isThrough); + toggleExtraClass(node, 'to', i === activeIndex); } }); const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent(); requestAnimationFrame(() => { - container.classList.add('animating'); + addExtraClass(container, 'animating'); onStart?.(); @@ -182,17 +186,20 @@ const Transition: FC = ({ return; } - container.classList.remove('animating', 'backwards'); + removeExtraClass(container, 'animating'); + removeExtraClass(container, 'backwards'); childNodes.forEach((node, i) => { if (node instanceof HTMLElement) { - node.classList.remove('from', 'through', 'to'); - node.classList.toggle(classNames.active, i === activeIndex); + removeExtraClass(node, 'from'); + removeExtraClass(node, 'through'); + removeExtraClass(node, 'to'); + toggleExtraClass(node, CLASSES.active, i === activeIndex); } }); if (shouldRestoreHeight) { - const activeElement = container.querySelector(`.${classNames.active}`); + const activeElement = container.querySelector(`.${CLASSES.active}`); if (activeElement) { activeElement.style.height = 'auto'; @@ -237,7 +244,7 @@ const Transition: FC = ({ useLayoutEffect(() => { if (shouldRestoreHeight) { const container = containerRef.current!; - const activeElement = container.querySelector(`.${classNames.active}`) + const activeElement = container.querySelector(`.${CLASSES.active}`) || container.querySelector('.from'); const clientHeight = activeElement?.clientHeight; if (!clientHeight) { @@ -250,6 +257,7 @@ const Transition: FC = ({ } }, [shouldRestoreHeight, children]); + const asFastList = !renderCount; const renders = rendersRef.current; const renderKeys = Object.keys(renderCount ? new Array(renderCount).fill(undefined) : renders).map(Number); const contents = renderKeys.map((key) => { @@ -273,7 +281,7 @@ const Transition: FC = ({ ref={containerRef} id={id} className={buildClassName('Transition', className, name)} - teactFastList={!renderCount} + teactFastList={asFastList} > {contents} @@ -303,11 +311,11 @@ function performSlideOptimized( if (animationLevel === ANIMATION_LEVEL_MIN) { fromSlide.style.transition = 'none'; fromSlide.style.transform = ''; - fromSlide.classList.remove(classNames.active); + removeExtraClass(fromSlide, CLASSES.active); toSlide.style.transition = 'none'; toSlide.style.transform = 'translate3d(0, 0, 0)'; - toSlide.classList.add(classNames.active); + addExtraClass(toSlide, CLASSES.active); cleanup(); @@ -337,8 +345,8 @@ function performSlideOptimized( toSlide.style.transition = ''; toSlide.style.transform = 'translate3d(0, 0, 0)'; - fromSlide.classList.remove(classNames.active); - toSlide.classList.add(classNames.active); + removeExtraClass(fromSlide, CLASSES.active); + addExtraClass(toSlide, CLASSES.active); waitForTransitionEnd(fromSlide, () => { if (activeKey !== currentKeyRef.current) { diff --git a/src/lib/teact/teact-dom.ts b/src/lib/teact/teact-dom.ts index 69817ee73..abd64b14e 100644 --- a/src/lib/teact/teact-dom.ts +++ b/src/lib/teact/teact-dom.ts @@ -43,6 +43,8 @@ const MAPPED_ATTRIBUTES: { [k: string]: string } = { const INDEX_KEY_PREFIX = '__indexKey#'; const headsByElement = new WeakMap(); +const extraClasses = new WeakMap>(); + // eslint-disable-next-line @typescript-eslint/naming-convention let DEBUG_virtualTreeSize = 1; @@ -319,15 +321,18 @@ function remount( } } -export function unmountRealTree($element: VirtualElement) { +function unmountRealTree($element: VirtualElement) { if (isComponentElement($element)) { unmountComponent($element.componentInstance); - } else { + } else if (!isFragmentElement($element)) { if (isTagElement($element)) { - if ($element.target) { - removeAllDelegatedListeners($element.target as HTMLElement); + const { target } = $element; - if ($element.props.ref?.current === $element.target) { + if (target) { + extraClasses.delete(target); + removeAllDelegatedListeners(target); + + if ($element.props.ref?.current === target) { $element.props.ref.current = undefined; } } @@ -629,10 +634,8 @@ function updateAttributes($current: VirtualElementTag, $new: VirtualElementTag, } function setAttribute(element: HTMLElement, key: string, value: any) { - // An optimization attempt if (key === 'className') { - element.className = value; - // An optimization attempt + updateClassName(element, value); } else if (key === 'value') { const inputEl = element as HTMLInputElement; @@ -668,7 +671,7 @@ function setAttribute(element: HTMLElement, key: string, value: any) { function removeAttribute(element: HTMLElement, key: string, value: any) { if (key === 'className') { - element.className = ''; + updateClassName(element, ''); } else if (key === 'value') { (element as HTMLInputElement).value = ''; } else if (key === 'style') { @@ -682,6 +685,55 @@ function removeAttribute(element: HTMLElement, key: string, value: any) { } } +function updateClassName(element: HTMLElement, value: string) { + const extra = extraClasses.get(element); + if (!extra) { + element.className = value; + return; + } + + const extraArray = Array.from(extra); + if (value) { + extraArray.push(value); + } + + element.className = extraArray.join(' '); +} + +export function addExtraClass(element: HTMLElement, className: string) { + element.classList.add(className); + + const classList = extraClasses.get(element); + if (classList) { + classList.add(className); + } else { + extraClasses.set(element, new Set([className])); + } +} + +export function removeExtraClass(element: HTMLElement, className: string) { + element.classList.remove(className); + + const classList = extraClasses.get(element); + if (classList) { + classList.delete(className); + + if (!classList.size) { + extraClasses.delete(element); + } + } +} + +export function toggleExtraClass(element: HTMLElement, className: string, force?: boolean) { + element.classList.toggle(className, force); + + if (element.classList.contains(className)) { + addExtraClass(element, className); + } else { + removeExtraClass(element, className); + } +} + // eslint-disable-next-line @typescript-eslint/naming-convention function DEBUG_addToVirtualTreeSize($current: VirtualElementParent | VirtualDomHead) { DEBUG_virtualTreeSize += $current.children.length; diff --git a/src/lib/teact/teact.ts b/src/lib/teact/teact.ts index 2e9b3d009..0a4917315 100644 --- a/src/lib/teact/teact.ts +++ b/src/lib/teact/teact.ts @@ -35,7 +35,7 @@ interface VirtualElementText { export interface VirtualElementTag { type: VirtualElementTypesEnum.Tag; - target?: Node; + target?: HTMLElement; tag: string; props: Props; children: VirtualElementChildren; @@ -50,7 +50,6 @@ export interface VirtualElementComponent { export interface VirtualElementFragment { type: VirtualElementTypesEnum.Fragment; - target?: Node; children: VirtualElementChildren; }