TelegramPWA/src/components/middle/MessageScroll.tsx

190 lines
5.2 KiB
TypeScript

import { MutableRefObject } from 'react';
import React, {
FC, useCallback, useEffect, useRef,
} from '../../lib/teact/teact';
import { MESSAGE_LIST_SENSITIVE_AREA } from '../../config';
import resetScroll from '../../util/resetScroll';
import { useIntersectionObserver, useOnIntersect } from '../../hooks/useIntersectionObserver';
import useOnChange from '../../hooks/useOnChange';
type OwnProps = {
containerRef: MutableRefObject<HTMLDivElement | null>;
className: string;
messageIds: number[];
containerHeight?: number;
listItemElementsRef: MutableRefObject<HTMLDivElement[] | undefined>;
anchorIdRef: MutableRefObject<string | undefined>;
anchorTopRef: MutableRefObject<number | undefined>;
loadMoreForwards?: NoneToVoidFunction;
loadMoreBackwards?: NoneToVoidFunction;
isViewportNewest?: boolean;
firstUnreadId?: number;
focusingId?: number;
onFabToggle: AnyToVoidFunction;
children: any;
};
const FAB_THRESHOLD = 50;
const FAB_FREEZE_TIMEOUT = 100;
// Local flag is used because `freeze/unfreeze` methods are controlled by heavy animation
let isFabFrozen = false;
const MessageScroll: FC<OwnProps> = ({
containerRef,
className,
messageIds,
containerHeight,
listItemElementsRef,
focusingId,
anchorIdRef,
anchorTopRef,
loadMoreForwards,
loadMoreBackwards,
isViewportNewest,
firstUnreadId,
onFabToggle,
children,
}) => {
// eslint-disable-next-line no-null/no-null
const backwardsTriggerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const forwardsTriggerRef = useRef<HTMLDivElement>(null);
// eslint-disable-next-line no-null/no-null
const fabTriggerRef = useRef<HTMLDivElement>(null);
const updateFabVisibility = useCallback(() => {
if (isFabFrozen) {
return;
}
if (!messageIds || !messageIds.length) {
onFabToggle(false);
return;
}
if (!isViewportNewest) {
onFabToggle(true);
return;
}
const { offsetHeight, scrollHeight, scrollTop } = containerRef.current!;
const scrollBottom = scrollHeight - scrollTop - offsetHeight;
const isNearBottom = scrollBottom <= FAB_THRESHOLD;
const isAtBottom = scrollBottom === 0;
onFabToggle(firstUnreadId ? !isAtBottom : !isNearBottom);
}, [messageIds, isViewportNewest, containerRef, onFabToggle, firstUnreadId]);
const {
observe: observeIntersection,
freeze: freezeForLoadMore,
unfreeze: unfreezeForLoadMore,
} = useIntersectionObserver({
rootRef: containerRef,
margin: MESSAGE_LIST_SENSITIVE_AREA,
}, (entries) => {
if (!loadMoreForwards || !loadMoreBackwards) {
return;
}
const triggerEntry = entries.find(({ isIntersecting }) => isIntersecting);
if (!triggerEntry) {
return;
}
const { target } = triggerEntry;
if (target.className === 'backwards-trigger') {
resetScroll(containerRef.current!);
loadMoreBackwards();
} else if (target.className === 'forwards-trigger' && (target as HTMLDivElement).dataset.isActive) {
resetScroll(containerRef.current!);
loadMoreForwards();
}
});
useOnIntersect(backwardsTriggerRef, observeIntersection);
useOnIntersect(forwardsTriggerRef, observeIntersection);
const {
observe: observeIntersectionForFab,
freeze: freezeForFab,
unfreeze: unfreezeForFab,
} = useIntersectionObserver({
rootRef: containerRef,
margin: FAB_THRESHOLD,
}, ([{ target }]) => {
if ((target as HTMLDivElement).dataset.isActive) {
updateFabVisibility();
}
});
useOnIntersect(fabTriggerRef, observeIntersectionForFab);
// Do not load more and show FAB when focusing
useOnChange(() => {
if (focusingId) {
freezeForLoadMore();
freezeForFab();
} else {
unfreezeForFab();
unfreezeForLoadMore();
}
}, [focusingId]);
// Remember scroll position before updating height
useOnChange(() => {
if (!listItemElementsRef.current) {
return;
}
const preservedItemElements = listItemElementsRef.current
.filter((element) => messageIds.includes(Number(element.dataset.messageId)));
// We avoid the very first item as it may be a partly-loaded album
// and also because it may be removed when messages limit is reached
const anchor = preservedItemElements[1] || preservedItemElements[0];
if (!anchor) {
return;
}
anchorIdRef.current = anchor.id;
anchorTopRef.current = anchor.getBoundingClientRect().top;
}, [messageIds, containerHeight]);
// Workaround for FAB flickering with tall incoming message
useOnChange(() => {
isFabFrozen = true;
setTimeout(() => {
isFabFrozen = false;
}, FAB_FREEZE_TIMEOUT);
}, [messageIds]);
// Workaround for stuck FAB when many unread messages
useEffect(updateFabVisibility, [firstUnreadId]);
return (
<div className={className} teactFastList>
<div ref={backwardsTriggerRef} key="backwards-trigger" className="backwards-trigger" />
{children}
<div
ref={forwardsTriggerRef}
key="forwards-trigger"
className="forwards-trigger"
data-is-active={!isViewportNewest}
/>
<div
ref={fabTriggerRef}
key="fab-trigger"
className="fab-trigger"
data-is-active={isViewportNewest}
/>
</div>
);
};
export default MessageScroll;