From 852d195842079d23bc06b085edf263ac17807738 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Sat, 1 May 2021 04:02:54 +0300 Subject: [PATCH] Message List: Better sending animation when multiline --- src/components/middle/MessageList.tsx | 24 ++++++++++++-- src/components/middle/composer/Composer.scss | 1 + .../middle/composer/MessageInput.tsx | 29 +++++++++++++--- src/util/fastSmoothScroll.ts | 33 +++++++++++++++---- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/src/components/middle/MessageList.tsx b/src/components/middle/MessageList.tsx index 9d963a65b..0ffcb097c 100644 --- a/src/components/middle/MessageList.tsx +++ b/src/components/middle/MessageList.tsx @@ -50,7 +50,7 @@ import useOnChange from '../../hooks/useOnChange'; import useStickyDates from './hooks/useStickyDates'; import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck'; import resetScroll from '../../util/resetScroll'; -import fastSmoothScroll from '../../util/fastSmoothScroll'; +import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll'; import renderText from '../common/helpers/renderText'; import useLang, { LangFn } from '../../hooks/useLang'; @@ -327,6 +327,10 @@ const MessageList: FC = ({ }; }, []); + useLayoutEffect(() => { + containerRef.current!.dataset.normalHeight = String(containerRef.current!.offsetHeight); + }, []); + // Workaround for an iOS bug when animated stickers sometimes disappear useLayoutEffect(() => { if (!IS_IOS) { @@ -410,10 +414,20 @@ const MessageList: FC = ({ const hasLastMessageChanged = ( messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1] ); - if (isAtBottom && hasLastMessageChanged && !hasFirstMessageChanged) { + + if (isAtBottom && hasLastMessageChanged && !hasFirstMessageChanged && !memoFocusingIdRef.current) { if (lastItemElement) { fastRaf(() => { - fastSmoothScroll(container, lastItemElement, 'end', BOTTOM_FOCUS_MARGIN); + fastSmoothScroll( + container, + lastItemElement, + 'end', + BOTTOM_FOCUS_MARGIN, + undefined, + undefined, + undefined, + true, + ); }); } @@ -440,6 +454,10 @@ const MessageList: FC = ({ ); if (isAtBottom && isResized) { + if (isAnimatingScroll()) { + return; + } + newScrollTop = scrollHeight - offsetHeight; } else if (anchor) { const newAnchorTop = anchor.getBoundingClientRect().top; diff --git a/src/components/middle/composer/Composer.scss b/src/components/middle/composer/Composer.scss index b1a2729de..3399026ea 100644 --- a/src/components/middle/composer/Composer.scss +++ b/src/components/middle/composer/Composer.scss @@ -317,6 +317,7 @@ caret-color: var(--color-text); min-height: 3.5rem; max-height: 26rem; + line-height: 1.3125; @media (max-width: 600px) { height: 2.875rem; diff --git a/src/components/middle/composer/MessageInput.tsx b/src/components/middle/composer/MessageInput.tsx index 3ffc2a10c..7caf41861 100644 --- a/src/components/middle/composer/MessageInput.tsx +++ b/src/components/middle/composer/MessageInput.tsx @@ -27,6 +27,7 @@ import TextFormatter from './TextFormatter'; const CONTEXT_MENU_CLOSE_DELAY_MS = 100; // Focus slows down animation, also it breaks transition layout in Chrome const FOCUS_DELAY_MS = 350; +const TRANSITION_DURATION_FACTOR = 50; type OwnProps = { id: string; @@ -104,7 +105,7 @@ const MessageInput: FC = ({ } if (prevHtml !== undefined && prevHtml !== html) { - updateInputHeight(); + updateInputHeight(!html.length); } }, [html]); @@ -270,11 +271,31 @@ const MessageInput: FC = ({ e.stopPropagation(); } - function updateInputHeight() { + function updateInputHeight(willSend = false) { const input = inputRef.current!; const clone = cloneRef.current!; - input.style.height = `${Math.min(clone.scrollHeight, MAX_INPUT_HEIGHT)}px`; - input.classList.toggle('overflown', clone.scrollHeight > MAX_INPUT_HEIGHT); + const currentHeight = Number(input.style.height.replace('px', '')); + const newHeight = Math.min(clone.scrollHeight, MAX_INPUT_HEIGHT); + if (newHeight === currentHeight) { + return; + } + + const transitionDuration = Math.round( + TRANSITION_DURATION_FACTOR * Math.log(Math.abs(newHeight - currentHeight)), + ); + + const exec = () => { + input.style.height = `${newHeight}px`; + input.style.transitionDuration = `${transitionDuration}ms`; + input.classList.toggle('overflown', clone.scrollHeight > MAX_INPUT_HEIGHT); + }; + + if (willSend) { + // Sync with sending animation + requestAnimationFrame(exec); + } else { + exec(); + } } useEffect(() => { diff --git a/src/util/fastSmoothScroll.ts b/src/util/fastSmoothScroll.ts index 61216d1d3..87aa3455c 100644 --- a/src/util/fastSmoothScroll.ts +++ b/src/util/fastSmoothScroll.ts @@ -8,6 +8,8 @@ const MAX_DISTANCE = 1500; const MIN_JS_DURATION = 250; const MAX_JS_DURATION = 600; +let isAnimating = false; + export default function fastSmoothScroll( container: HTMLElement, element: HTMLElement, @@ -16,6 +18,7 @@ export default function fastSmoothScroll( maxDistance = MAX_DISTANCE, forceDirection?: FocusDirection, forceDuration?: number, + forceCurrentContainerHeight?: boolean, ) { if (forceDirection === FocusDirection.Static) { element.scrollIntoView({ block: position }); @@ -39,17 +42,31 @@ export default function fastSmoothScroll( container.scrollTop = Math.max(0, offsetTop - maxDistance); } + isAnimating = true; fastRaf(() => { dispatchHeavyAnimationEvent(MAX_JS_DURATION); - scrollWithJs(container, element, position, margin, forceDuration); + scrollWithJs(container, element, position, margin, forceDuration, forceCurrentContainerHeight); }); } +export function isAnimatingScroll() { + return isAnimating; +} + function scrollWithJs( - container: HTMLElement, element: HTMLElement, position: ScrollLogicalPosition, margin = 0, forceDuration?: number, + container: HTMLElement, + element: HTMLElement, + position: ScrollLogicalPosition, + margin = 0, + forceDuration?: number, + forceCurrentContainerHeight?: boolean, ) { const { offsetTop: elementTop, offsetHeight: elementHeight } = element; const { scrollTop, offsetHeight: containerHeight, scrollHeight } = container; + const targetContainerHeight = !forceCurrentContainerHeight && container.dataset.normalHeight + ? Number(container.dataset.normalHeight) + : containerHeight; + let path!: number; switch (position) { @@ -57,13 +74,13 @@ function scrollWithJs( path = (elementTop - margin) - scrollTop; break; case 'end': - path = (elementTop + elementHeight + margin) - (scrollTop + containerHeight); + path = (elementTop + elementHeight + margin) - (scrollTop + targetContainerHeight); break; // 'nearest' is not supported yet case 'nearest': case 'center': - path = elementHeight < containerHeight - ? (elementTop + elementHeight / 2) - (scrollTop + containerHeight / 2) + path = elementHeight < targetContainerHeight + ? (elementTop + elementHeight / 2) - (scrollTop + targetContainerHeight / 2) : (elementTop - margin) - scrollTop; break; } @@ -72,7 +89,7 @@ function scrollWithJs( const remainingPath = -scrollTop; path = Math.max(path, remainingPath); } else if (path > 0) { - const remainingPath = scrollHeight - (scrollTop + containerHeight); + const remainingPath = scrollHeight - (scrollTop + targetContainerHeight); path = Math.min(path, remainingPath); } @@ -88,7 +105,9 @@ function scrollWithJs( const currentPath = path * (1 - transition(t)); container.scrollTop = Math.round(target - currentPath); - return t < 1; + isAnimating = t < 1; + + return isAnimating; }); }