Message List: Better sending animation when multiline

This commit is contained in:
Alexander Zinchuk 2021-05-01 04:02:54 +03:00
parent 5c2925f1ed
commit 852d195842
4 changed files with 73 additions and 14 deletions

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
};
}, []);
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<OwnProps & StateProps & DispatchProps> = ({
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<OwnProps & StateProps & DispatchProps> = ({
);
if (isAtBottom && isResized) {
if (isAnimatingScroll()) {
return;
}
newScrollTop = scrollHeight - offsetHeight;
} else if (anchor) {
const newAnchorTop = anchor.getBoundingClientRect().top;

View File

@ -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;

View File

@ -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<OwnProps & StateProps & DispatchProps> = ({
}
if (prevHtml !== undefined && prevHtml !== html) {
updateInputHeight();
updateInputHeight(!html.length);
}
}, [html]);
@ -270,11 +271,31 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
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(() => {

View File

@ -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;
});
}