302 lines
9.2 KiB
TypeScript
302 lines
9.2 KiB
TypeScript
import React, {
|
|
memo, useCallback, useEffect, useMemo, useRef, useState,
|
|
} from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
import { requestNextMutation } from '../../../lib/fasterdom/fasterdom';
|
|
|
|
import type { FC } from '../../../lib/teact/teact';
|
|
import type { ApiChat } from '../../../api/types';
|
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
|
|
|
import {
|
|
GENERAL_TOPIC_ID, TOPICS_SLICE, TOPIC_HEIGHT_PX, TOPIC_LIST_SENSITIVE_AREA,
|
|
} from '../../../config';
|
|
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
|
import {
|
|
selectCanAnimateInterface, selectChat, selectCurrentMessageList, selectIsForumPanelOpen, selectTabState,
|
|
} from '../../../global/selectors';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { getOrderedTopics } from '../../../global/helpers';
|
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
|
import { waitForTransitionEnd } from '../../../util/cssAnimationEndListeners';
|
|
import { captureEvents, SwipeDirection } from '../../../util/captureEvents';
|
|
import { createLocationHash } from '../../../util/routing';
|
|
|
|
import useInfiniteScroll from '../../../hooks/useInfiniteScroll';
|
|
import { useIntersectionObserver, useOnIntersect } from '../../../hooks/useIntersectionObserver';
|
|
import useOrderDiff from './hooks/useOrderDiff';
|
|
import useLang from '../../../hooks/useLang';
|
|
import usePrevious from '../../../hooks/usePrevious';
|
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
|
import { dispatchHeavyAnimationEvent } from '../../../hooks/useHeavyAnimationCheck';
|
|
import useAppLayout from '../../../hooks/useAppLayout';
|
|
|
|
import GroupChatInfo from '../../common/GroupChatInfo';
|
|
import Button from '../../ui/Button';
|
|
import Topic from './Topic';
|
|
import InfiniteScroll from '../../ui/InfiniteScroll';
|
|
import Loading from '../../ui/Loading';
|
|
import HeaderActions from '../../middle/HeaderActions';
|
|
import GroupCallTopPane from '../../calls/group/GroupCallTopPane';
|
|
import EmptyForum from './EmptyForum';
|
|
|
|
import styles from './ForumPanel.module.scss';
|
|
|
|
type OwnProps = {
|
|
isOpen?: boolean;
|
|
isHidden?: boolean;
|
|
onTopicSearch?: NoneToVoidFunction;
|
|
onCloseAnimationEnd?: VoidFunction;
|
|
onOpenAnimationStart?: VoidFunction;
|
|
};
|
|
|
|
type StateProps = {
|
|
chat?: ApiChat;
|
|
currentTopicId?: number;
|
|
lastSyncTime?: number;
|
|
withInterfaceAnimations?: boolean;
|
|
};
|
|
|
|
const INTERSECTION_THROTTLE = 200;
|
|
|
|
const ForumPanel: FC<OwnProps & StateProps> = ({
|
|
chat,
|
|
currentTopicId,
|
|
isOpen,
|
|
isHidden,
|
|
lastSyncTime,
|
|
onTopicSearch,
|
|
onCloseAnimationEnd,
|
|
onOpenAnimationStart,
|
|
withInterfaceAnimations,
|
|
}) => {
|
|
const {
|
|
closeForumPanel, openChatWithInfo, loadTopics,
|
|
} = getActions();
|
|
|
|
// eslint-disable-next-line no-null/no-null
|
|
const ref = useRef<HTMLDivElement>(null);
|
|
|
|
// eslint-disable-next-line no-null/no-null
|
|
const containerRef = useRef<HTMLDivElement>(null);
|
|
// eslint-disable-next-line no-null/no-null
|
|
const scrollTopHandlerRef = useRef<HTMLDivElement>(null);
|
|
const { isMobile } = useAppLayout();
|
|
|
|
useEffect(() => {
|
|
if (lastSyncTime && chat && !chat.topics) {
|
|
loadTopics({ chatId: chat.id });
|
|
}
|
|
}, [chat, lastSyncTime, loadTopics]);
|
|
|
|
const [isScrolled, setIsScrolled] = useState(false);
|
|
const lang = useLang();
|
|
|
|
const handleClose = useCallback(() => {
|
|
closeForumPanel();
|
|
}, [closeForumPanel]);
|
|
|
|
useEffect(() => {
|
|
if (!withInterfaceAnimations && !isOpen) {
|
|
onCloseAnimationEnd?.();
|
|
}
|
|
}, [withInterfaceAnimations, isOpen, onCloseAnimationEnd]);
|
|
|
|
const handleToggleChatInfo = useCallback(() => {
|
|
if (!chat) return;
|
|
openChatWithInfo({ id: chat.id, shouldReplaceHistory: true });
|
|
}, [chat, openChatWithInfo]);
|
|
|
|
const { observe } = useIntersectionObserver({
|
|
rootRef: containerRef,
|
|
throttleMs: INTERSECTION_THROTTLE,
|
|
});
|
|
|
|
useOnIntersect(scrollTopHandlerRef, observe, ({ isIntersecting }) => {
|
|
setIsScrolled(!isIntersecting);
|
|
});
|
|
|
|
const orderedIds = useMemo(() => {
|
|
return chat?.topics
|
|
? getOrderedTopics(Object.values(chat.topics), chat.orderedPinnedTopicIds).map(({ id }) => id)
|
|
: [];
|
|
}, [chat]);
|
|
|
|
const { orderDiffById, getAnimationType } = useOrderDiff(orderedIds, chat?.id);
|
|
|
|
const [viewportIds, getMore] = useInfiniteScroll(() => {
|
|
if (!chat || !lastSyncTime) return;
|
|
loadTopics({ chatId: chat.id });
|
|
}, orderedIds, !chat?.topicsCount || orderedIds.length >= chat.topicsCount, TOPICS_SLICE);
|
|
|
|
const shouldRenderRef = useRef(false);
|
|
const isVisible = isOpen && !isHidden;
|
|
const prevIsVisible = usePrevious(isVisible);
|
|
|
|
if (prevIsVisible !== isVisible) {
|
|
shouldRenderRef.current = false;
|
|
}
|
|
|
|
useHistoryBack({
|
|
isActive: isVisible,
|
|
onBack: handleClose,
|
|
hash: chat ? createLocationHash(chat.id, 'thread', MAIN_THREAD_ID) : undefined,
|
|
});
|
|
|
|
useEffect(() => (isVisible ? captureEscKeyListener(handleClose) : undefined), [handleClose, isVisible]);
|
|
|
|
useEffect(() => {
|
|
if (prevIsVisible !== isVisible) {
|
|
// For performance reasons, we delay animation of the topic list panel to the next animation frame
|
|
requestNextMutation(() => {
|
|
if (!ref.current) return;
|
|
|
|
const dispatchHeavyAnimationStop = dispatchHeavyAnimationEvent();
|
|
waitForTransitionEnd(ref.current, dispatchHeavyAnimationStop);
|
|
|
|
onOpenAnimationStart?.();
|
|
|
|
if (isVisible) {
|
|
shouldRenderRef.current = true;
|
|
ref.current!.style.transform = 'none';
|
|
} else {
|
|
shouldRenderRef.current = false;
|
|
ref.current!.style.transform = '';
|
|
}
|
|
});
|
|
}
|
|
}, [isVisible, onOpenAnimationStart, prevIsVisible]);
|
|
|
|
useEffect(() => {
|
|
if (!IS_TOUCH_ENV) {
|
|
return undefined;
|
|
}
|
|
|
|
return captureEvents(ref.current!, {
|
|
selectorToPreventScroll: '.chat-list',
|
|
onSwipe: ((e, direction) => {
|
|
const closeDirection = lang.isRtl ? SwipeDirection.Left : SwipeDirection.Right;
|
|
|
|
if (direction === closeDirection) {
|
|
closeForumPanel();
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}),
|
|
});
|
|
}, [closeForumPanel, lang.isRtl]);
|
|
|
|
function renderTopics() {
|
|
const viewportOffset = orderedIds!.indexOf(viewportIds![0]);
|
|
|
|
return viewportIds?.map((id, i) => (
|
|
<Topic
|
|
key={id}
|
|
chatId={chat!.id}
|
|
topic={chat!.topics![id]}
|
|
style={`top: ${(viewportOffset + i) * TOPIC_HEIGHT_PX}px;`}
|
|
isSelected={currentTopicId === id}
|
|
observeIntersection={observe}
|
|
animationType={getAnimationType(id)}
|
|
orderDiff={orderDiffById[id]}
|
|
/>
|
|
));
|
|
}
|
|
|
|
const isLoading = chat?.topics === undefined;
|
|
|
|
return (
|
|
<div
|
|
ref={ref}
|
|
className={buildClassName(
|
|
styles.root,
|
|
isScrolled && styles.scrolled,
|
|
lang.isRtl && styles.rtl,
|
|
!withInterfaceAnimations && styles.noAnimation,
|
|
)}
|
|
onTransitionEnd={!isOpen ? onCloseAnimationEnd : undefined}
|
|
>
|
|
<div id="TopicListHeader" className="left-header">
|
|
<Button
|
|
round
|
|
size="smaller"
|
|
color="translucent"
|
|
onClick={handleClose}
|
|
ariaLabel={lang('Close')}
|
|
>
|
|
<i className="icon icon-close" />
|
|
</Button>
|
|
|
|
{chat && (
|
|
<GroupChatInfo
|
|
noAvatar
|
|
className={styles.info}
|
|
chatId={chat.id}
|
|
onClick={handleToggleChatInfo}
|
|
/>
|
|
)}
|
|
|
|
{chat
|
|
&& (
|
|
<HeaderActions
|
|
chatId={chat.id}
|
|
threadId={MAIN_THREAD_ID}
|
|
messageListType="thread"
|
|
canExpandActions={false}
|
|
withForumActions
|
|
isMobile={isMobile}
|
|
onTopicSearch={onTopicSearch}
|
|
/>
|
|
)}
|
|
</div>
|
|
|
|
{chat && <GroupCallTopPane chatId={chat.id} hasPinnedOffset={false} className={styles.groupCall} />}
|
|
|
|
<div className={styles.notch} />
|
|
|
|
<InfiniteScroll
|
|
className="chat-list custom-scroll"
|
|
ref={containerRef}
|
|
items={viewportIds}
|
|
preloadBackwards={TOPICS_SLICE}
|
|
withAbsolutePositioning
|
|
maxHeight={(orderedIds?.length || 0) * TOPIC_HEIGHT_PX}
|
|
onLoadMore={getMore}
|
|
sensitiveArea={TOPIC_LIST_SENSITIVE_AREA}
|
|
beforeChildren={<div ref={scrollTopHandlerRef} className={styles.scrollTopHandler} />}
|
|
>
|
|
{Boolean(viewportIds?.length) && (
|
|
renderTopics()
|
|
)}
|
|
{isLoading && !viewportIds?.length && (
|
|
<Loading key="loading" />
|
|
)}
|
|
</InfiniteScroll>
|
|
{!isLoading && viewportIds?.length === 1 && viewportIds[0] === GENERAL_TOPIC_ID && (
|
|
<EmptyForum chatId={chat.id} />
|
|
)}
|
|
</div>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, ownProps, detachWhenChanged): StateProps => {
|
|
detachWhenChanged(selectIsForumPanelOpen(global));
|
|
|
|
const chatId = selectTabState(global).forumPanelChatId;
|
|
const chat = chatId ? selectChat(global, chatId) : undefined;
|
|
const {
|
|
chatId: currentChatId,
|
|
threadId: currentThreadId,
|
|
} = selectCurrentMessageList(global) || {};
|
|
|
|
return {
|
|
chat,
|
|
lastSyncTime: global.lastSyncTime,
|
|
currentTopicId: chatId === currentChatId ? currentThreadId : undefined,
|
|
withInterfaceAnimations: selectCanAnimateInterface(global),
|
|
};
|
|
},
|
|
)(ForumPanel));
|