Message List: Better sending animation when multiline
This commit is contained in:
parent
5c2925f1ed
commit
852d195842
@ -50,7 +50,7 @@ import useOnChange from '../../hooks/useOnChange';
|
|||||||
import useStickyDates from './hooks/useStickyDates';
|
import useStickyDates from './hooks/useStickyDates';
|
||||||
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
import { dispatchHeavyAnimationEvent } from '../../hooks/useHeavyAnimationCheck';
|
||||||
import resetScroll from '../../util/resetScroll';
|
import resetScroll from '../../util/resetScroll';
|
||||||
import fastSmoothScroll from '../../util/fastSmoothScroll';
|
import fastSmoothScroll, { isAnimatingScroll } from '../../util/fastSmoothScroll';
|
||||||
import renderText from '../common/helpers/renderText';
|
import renderText from '../common/helpers/renderText';
|
||||||
import useLang, { LangFn } from '../../hooks/useLang';
|
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
|
// Workaround for an iOS bug when animated stickers sometimes disappear
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
if (!IS_IOS) {
|
if (!IS_IOS) {
|
||||||
@ -410,10 +414,20 @@ const MessageList: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
const hasLastMessageChanged = (
|
const hasLastMessageChanged = (
|
||||||
messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1]
|
messageIds && prevMessageIds && messageIds[messageIds.length - 1] !== prevMessageIds[prevMessageIds.length - 1]
|
||||||
);
|
);
|
||||||
if (isAtBottom && hasLastMessageChanged && !hasFirstMessageChanged) {
|
|
||||||
|
if (isAtBottom && hasLastMessageChanged && !hasFirstMessageChanged && !memoFocusingIdRef.current) {
|
||||||
if (lastItemElement) {
|
if (lastItemElement) {
|
||||||
fastRaf(() => {
|
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 (isAtBottom && isResized) {
|
||||||
|
if (isAnimatingScroll()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
newScrollTop = scrollHeight - offsetHeight;
|
newScrollTop = scrollHeight - offsetHeight;
|
||||||
} else if (anchor) {
|
} else if (anchor) {
|
||||||
const newAnchorTop = anchor.getBoundingClientRect().top;
|
const newAnchorTop = anchor.getBoundingClientRect().top;
|
||||||
|
|||||||
@ -317,6 +317,7 @@
|
|||||||
caret-color: var(--color-text);
|
caret-color: var(--color-text);
|
||||||
min-height: 3.5rem;
|
min-height: 3.5rem;
|
||||||
max-height: 26rem;
|
max-height: 26rem;
|
||||||
|
line-height: 1.3125;
|
||||||
|
|
||||||
@media (max-width: 600px) {
|
@media (max-width: 600px) {
|
||||||
height: 2.875rem;
|
height: 2.875rem;
|
||||||
|
|||||||
@ -27,6 +27,7 @@ import TextFormatter from './TextFormatter';
|
|||||||
const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
|
const CONTEXT_MENU_CLOSE_DELAY_MS = 100;
|
||||||
// Focus slows down animation, also it breaks transition layout in Chrome
|
// Focus slows down animation, also it breaks transition layout in Chrome
|
||||||
const FOCUS_DELAY_MS = 350;
|
const FOCUS_DELAY_MS = 350;
|
||||||
|
const TRANSITION_DURATION_FACTOR = 50;
|
||||||
|
|
||||||
type OwnProps = {
|
type OwnProps = {
|
||||||
id: string;
|
id: string;
|
||||||
@ -104,7 +105,7 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (prevHtml !== undefined && prevHtml !== html) {
|
if (prevHtml !== undefined && prevHtml !== html) {
|
||||||
updateInputHeight();
|
updateInputHeight(!html.length);
|
||||||
}
|
}
|
||||||
}, [html]);
|
}, [html]);
|
||||||
|
|
||||||
@ -270,11 +271,31 @@ const MessageInput: FC<OwnProps & StateProps & DispatchProps> = ({
|
|||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateInputHeight() {
|
function updateInputHeight(willSend = false) {
|
||||||
const input = inputRef.current!;
|
const input = inputRef.current!;
|
||||||
const clone = cloneRef.current!;
|
const clone = cloneRef.current!;
|
||||||
input.style.height = `${Math.min(clone.scrollHeight, MAX_INPUT_HEIGHT)}px`;
|
const currentHeight = Number(input.style.height.replace('px', ''));
|
||||||
input.classList.toggle('overflown', clone.scrollHeight > MAX_INPUT_HEIGHT);
|
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(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@ -8,6 +8,8 @@ const MAX_DISTANCE = 1500;
|
|||||||
const MIN_JS_DURATION = 250;
|
const MIN_JS_DURATION = 250;
|
||||||
const MAX_JS_DURATION = 600;
|
const MAX_JS_DURATION = 600;
|
||||||
|
|
||||||
|
let isAnimating = false;
|
||||||
|
|
||||||
export default function fastSmoothScroll(
|
export default function fastSmoothScroll(
|
||||||
container: HTMLElement,
|
container: HTMLElement,
|
||||||
element: HTMLElement,
|
element: HTMLElement,
|
||||||
@ -16,6 +18,7 @@ export default function fastSmoothScroll(
|
|||||||
maxDistance = MAX_DISTANCE,
|
maxDistance = MAX_DISTANCE,
|
||||||
forceDirection?: FocusDirection,
|
forceDirection?: FocusDirection,
|
||||||
forceDuration?: number,
|
forceDuration?: number,
|
||||||
|
forceCurrentContainerHeight?: boolean,
|
||||||
) {
|
) {
|
||||||
if (forceDirection === FocusDirection.Static) {
|
if (forceDirection === FocusDirection.Static) {
|
||||||
element.scrollIntoView({ block: position });
|
element.scrollIntoView({ block: position });
|
||||||
@ -39,17 +42,31 @@ export default function fastSmoothScroll(
|
|||||||
container.scrollTop = Math.max(0, offsetTop - maxDistance);
|
container.scrollTop = Math.max(0, offsetTop - maxDistance);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
isAnimating = true;
|
||||||
fastRaf(() => {
|
fastRaf(() => {
|
||||||
dispatchHeavyAnimationEvent(MAX_JS_DURATION);
|
dispatchHeavyAnimationEvent(MAX_JS_DURATION);
|
||||||
scrollWithJs(container, element, position, margin, forceDuration);
|
scrollWithJs(container, element, position, margin, forceDuration, forceCurrentContainerHeight);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAnimatingScroll() {
|
||||||
|
return isAnimating;
|
||||||
|
}
|
||||||
|
|
||||||
function scrollWithJs(
|
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 { offsetTop: elementTop, offsetHeight: elementHeight } = element;
|
||||||
const { scrollTop, offsetHeight: containerHeight, scrollHeight } = container;
|
const { scrollTop, offsetHeight: containerHeight, scrollHeight } = container;
|
||||||
|
const targetContainerHeight = !forceCurrentContainerHeight && container.dataset.normalHeight
|
||||||
|
? Number(container.dataset.normalHeight)
|
||||||
|
: containerHeight;
|
||||||
|
|
||||||
let path!: number;
|
let path!: number;
|
||||||
|
|
||||||
switch (position) {
|
switch (position) {
|
||||||
@ -57,13 +74,13 @@ function scrollWithJs(
|
|||||||
path = (elementTop - margin) - scrollTop;
|
path = (elementTop - margin) - scrollTop;
|
||||||
break;
|
break;
|
||||||
case 'end':
|
case 'end':
|
||||||
path = (elementTop + elementHeight + margin) - (scrollTop + containerHeight);
|
path = (elementTop + elementHeight + margin) - (scrollTop + targetContainerHeight);
|
||||||
break;
|
break;
|
||||||
// 'nearest' is not supported yet
|
// 'nearest' is not supported yet
|
||||||
case 'nearest':
|
case 'nearest':
|
||||||
case 'center':
|
case 'center':
|
||||||
path = elementHeight < containerHeight
|
path = elementHeight < targetContainerHeight
|
||||||
? (elementTop + elementHeight / 2) - (scrollTop + containerHeight / 2)
|
? (elementTop + elementHeight / 2) - (scrollTop + targetContainerHeight / 2)
|
||||||
: (elementTop - margin) - scrollTop;
|
: (elementTop - margin) - scrollTop;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@ -72,7 +89,7 @@ function scrollWithJs(
|
|||||||
const remainingPath = -scrollTop;
|
const remainingPath = -scrollTop;
|
||||||
path = Math.max(path, remainingPath);
|
path = Math.max(path, remainingPath);
|
||||||
} else if (path > 0) {
|
} else if (path > 0) {
|
||||||
const remainingPath = scrollHeight - (scrollTop + containerHeight);
|
const remainingPath = scrollHeight - (scrollTop + targetContainerHeight);
|
||||||
path = Math.min(path, remainingPath);
|
path = Math.min(path, remainingPath);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -88,7 +105,9 @@ function scrollWithJs(
|
|||||||
const currentPath = path * (1 - transition(t));
|
const currentPath = path * (1 - transition(t));
|
||||||
container.scrollTop = Math.round(target - currentPath);
|
container.scrollTop = Math.round(target - currentPath);
|
||||||
|
|
||||||
return t < 1;
|
isAnimating = t < 1;
|
||||||
|
|
||||||
|
return isAnimating;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user