2023-01-06 22:49:02 +01:00

412 lines
13 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, {
memo, useCallback, useEffect, useRef,
} from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type {
ApiChat,
ApiUser,
ApiMessage,
ApiMessageOutgoingStatus,
ApiFormattedText,
ApiUserStatus,
ApiTopic,
ApiTypingStatus,
} from '../../../api/types';
import type { AnimationLevel } from '../../../types';
import type { ChatAnimationTypes } from './hooks';
import { ANIMATION_END_DELAY } from '../../../config';
import { MAIN_THREAD_ID } from '../../../api/types';
import { IS_SINGLE_COLUMN_LAYOUT } from '../../../util/environment';
import {
isUserId,
getPrivateChatUserId,
getMessageAction,
selectIsChatMuted,
} from '../../../global/helpers';
import {
selectChat,
selectUser,
selectChatMessage,
selectOutgoingStatus,
selectDraft,
selectCurrentMessageList,
selectNotifySettings,
selectNotifyExceptions,
selectUserStatus,
selectIsDefaultEmojiStatusPack,
selectTopicFromMessage,
selectThreadParam,
selectIsForumPanelOpen,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { fastRaf } from '../../../util/schedulers';
import buildStyle from '../../../util/buildStyle';
import useChatContextActions from '../../../hooks/useChatContextActions';
import useFlag from '../../../hooks/useFlag';
import useChatListEntry from './hooks/useChatListEntry';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import usePrevious from '../../../hooks/usePrevious';
import Avatar from '../../common/Avatar';
import LastMessageMeta from '../../common/LastMessageMeta';
import DeleteChatModal from '../../common/DeleteChatModal';
import ListItem from '../../ui/ListItem';
import Badge from './Badge';
import ChatFolderModal from '../ChatFolderModal.async';
import ChatCallStatus from './ChatCallStatus';
import ReportModal from '../../common/ReportModal';
import FullNameTitle from '../../common/FullNameTitle';
import './Chat.scss';
const TRANSFORM_TO_TOPIC_LIST_ANIMATION_DELAY = 300;
type OwnProps = {
chatId: string;
folderId?: number;
orderDiff: number;
animationType: ChatAnimationTypes;
isPinned?: boolean;
offsetTopInSmallerMode: number;
offsetTop: number;
observeIntersection?: ObserveFn;
onDragEnter?: (chatId: string) => void;
};
type StateProps = {
chat?: ApiChat;
isMuted?: boolean;
user?: ApiUser;
userStatus?: ApiUserStatus;
isEmojiStatusColored?: boolean;
actionTargetUserIds?: string[];
actionTargetMessage?: ApiMessage;
actionTargetChatId?: string;
lastMessageSender?: ApiUser | ApiChat;
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
draft?: ApiFormattedText;
animationLevel?: AnimationLevel;
isSelected?: boolean;
isForumPanelActive?: boolean;
canScrollDown?: boolean;
canChangeFolder?: boolean;
lastSyncTime?: number;
lastMessageTopic?: ApiTopic;
typingStatus?: ApiTypingStatus;
forumPanelChatId?: string;
};
const Chat: FC<OwnProps & StateProps> = ({
chatId,
folderId,
orderDiff,
animationType,
isPinned,
observeIntersection,
chat,
isMuted,
user,
userStatus,
isEmojiStatusColored,
actionTargetUserIds,
lastMessageSender,
lastMessageOutgoingStatus,
actionTargetMessage,
actionTargetChatId,
offsetTopInSmallerMode,
offsetTop,
draft,
animationLevel,
isSelected,
isForumPanelActive,
canScrollDown,
canChangeFolder,
lastSyncTime,
lastMessageTopic,
typingStatus,
forumPanelChatId,
onDragEnter,
}) => {
const {
openChat,
openForumPanel,
closeForumPanel,
focusLastMessage,
loadTopics,
} = getActions();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag();
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag();
const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag();
const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag();
const { lastMessage, isForum } = chat || {};
const { renderSubtitle, ref } = useChatListEntry({
chat,
chatId,
lastMessage,
typingStatus,
draft,
actionTargetMessage,
actionTargetUserIds,
actionTargetChatId,
lastMessageTopic,
lastMessageSender,
observeIntersection,
animationType,
animationLevel,
orderDiff,
});
const handleClick = useCallback(() => {
if (chat?.isForum) {
openForumPanel({ chatId });
return;
}
if (forumPanelChatId) closeForumPanel();
openChat({ id: chatId, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true });
if (isSelected && canScrollDown) {
focusLastMessage();
}
}, [
chat?.isForum, forumPanelChatId, closeForumPanel, openChat, chatId, isSelected, canScrollDown, openForumPanel,
focusLastMessage,
]);
const handleDragEnter = useCallback((e) => {
e.preventDefault();
onDragEnter?.(chatId);
}, [chatId, onDragEnter]);
const handleDelete = useCallback(() => {
markRenderDeleteModal();
openDeleteModal();
}, [markRenderDeleteModal, openDeleteModal]);
const handleChatFolderChange = useCallback(() => {
markRenderChatFolderModal();
openChatFolderModal();
}, [markRenderChatFolderModal, openChatFolderModal]);
const handleReport = useCallback(() => {
markRenderReportModal();
openReportModal();
}, [markRenderReportModal, openReportModal]);
const contextActions = useChatContextActions({
chat,
user,
handleDelete,
handleChatFolderChange,
handleReport,
folderId,
isPinned,
isMuted,
canChangeFolder,
});
const isIntersecting = useIsIntersecting(ref, observeIntersection);
// Load the forum topics to display unread count badge
useEffect(() => {
if (isIntersecting && lastSyncTime && isForum && chat && chat.topics === undefined) {
loadTopics({ chatId });
}
}, [chat, chatId, isForum, isIntersecting, lastSyncTime, loadTopics]);
const isOnForumPanel = chatId === forumPanelChatId;
const prevIsForumPanelActive = usePrevious(isForumPanelActive);
const isAnimatingRef = useRef(false);
if (prevIsForumPanelActive !== isForumPanelActive) {
isAnimatingRef.current = true;
}
// Animate changing to smaller chat size when navigating to/from forum topic list
useEffect(() => {
const current = ref.current;
if (current && isAnimatingRef.current && isForumPanelActive !== prevIsForumPanelActive) {
current.classList.add('animate-transform');
current.style.transform = '';
setTimeout(() => {
// Wait one more frame for better animation performance
fastRaf(() => {
isAnimatingRef.current = false;
current.classList.remove('animate-transform');
});
}, TRANSFORM_TO_TOPIC_LIST_ANIMATION_DELAY + ANIMATION_END_DELAY);
}
}, [ref, isForumPanelActive, prevIsForumPanelActive]);
if (!chat) {
return undefined;
}
const className = buildClassName(
'Chat chat-item-clickable',
isUserId(chatId) ? 'private' : 'group',
isForum && 'forum',
isSelected && 'selected',
isForumPanelActive && 'smaller',
isOnForumPanel && 'active-forum',
);
const chatTop = isForumPanelActive ? (offsetTop - offsetTopInSmallerMode) : offsetTop;
const offsetAnimate = isForumPanelActive ? offsetTopInSmallerMode : -offsetTopInSmallerMode;
return (
<ListItem
ref={ref}
className={className}
style={buildStyle(`top: ${chatTop}px`, isAnimatingRef.current && `transform: translateY(${offsetAnimate}px)`)}
ripple={!isForum && !IS_SINGLE_COLUMN_LAYOUT}
contextActions={contextActions}
onClick={handleClick}
onDragEnter={handleDragEnter}
shouldUsePortalForMenu={isForumPanelActive}
>
<div className="status">
<Avatar
chat={chat}
user={user}
userStatus={userStatus}
isSavedMessages={user?.isSelf}
lastSyncTime={lastSyncTime}
animationLevel={animationLevel}
withVideo
observeIntersection={observeIntersection}
/>
<div className={buildClassName(
'status-badge-wrapper',
isForumPanelActive && 'status-badge-wrapper-visible',
)}
>
<Badge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant />
</div>
{chat.isCallActive && chat.isCallNotEmpty && (
<ChatCallStatus isSelected={isSelected} isActive={animationLevel !== 0} />
)}
</div>
<div className="info">
<div className="info-row">
<FullNameTitle
peer={user || chat}
withEmojiStatus
isSavedMessages={chatId === user?.id && user?.isSelf}
observeIntersection={observeIntersection}
key={!IS_SINGLE_COLUMN_LAYOUT && isEmojiStatusColored ? `${isSelected}` : undefined}
/>
{isMuted && <i className="icon-muted" />}
<div className="separator" />
{chat.lastMessage && (
<LastMessageMeta
message={chat.lastMessage}
outgoingStatus={lastMessageOutgoingStatus}
/>
)}
</div>
<div className="subtitle">
{renderSubtitle()}
<Badge chat={chat} isPinned={isPinned} isMuted={isMuted} />
</div>
</div>
{shouldRenderDeleteModal && (
<DeleteChatModal
isOpen={isDeleteModalOpen}
onClose={closeDeleteModal}
onCloseAnimationEnd={unmarkRenderDeleteModal}
chat={chat}
/>
)}
{shouldRenderChatFolderModal && (
<ChatFolderModal
isOpen={isChatFolderModalOpen}
onClose={closeChatFolderModal}
onCloseAnimationEnd={unmarkRenderChatFolderModal}
chatId={chatId}
/>
)}
{shouldRenderReportModal && (
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
onCloseAnimationEnd={unmarkRenderReportModal}
chatId={chatId}
subject="peer"
/>
)}
</ListItem>
);
};
export default memo(withGlobal<OwnProps>(
(global, { chatId }): StateProps => {
const chat = selectChat(global, chatId);
if (!chat) {
return {};
}
const { senderId, replyToMessageId, isOutgoing } = chat.lastMessage || {};
const lastMessageSender = senderId
? (selectUser(global, senderId) || selectChat(global, senderId)) : undefined;
const lastMessageAction = chat.lastMessage ? getMessageAction(chat.lastMessage) : undefined;
const actionTargetMessage = lastMessageAction && replyToMessageId
? selectChatMessage(global, chat.id, replyToMessageId)
: undefined;
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
const privateChatUserId = getPrivateChatUserId(chat);
const {
chatId: currentChatId,
threadId: currentThreadId,
type: messageListType,
} = selectCurrentMessageList(global) || {};
const isForumPanelActive = selectIsForumPanelOpen(global);
const isSelected = chatId === currentChatId && currentThreadId === MAIN_THREAD_ID;
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;
const statusEmoji = user?.emojiStatus && global.customEmojis.byId[user.emojiStatus.documentId];
const isEmojiStatusColored = statusEmoji && selectIsDefaultEmojiStatusPack(global, statusEmoji.stickerSetInfo);
const lastMessageTopic = chat.lastMessage && selectTopicFromMessage(global, chat.lastMessage);
const typingStatus = selectThreadParam(global, chatId, MAIN_THREAD_ID, 'typingStatus');
return {
chat,
isMuted: selectIsChatMuted(chat, selectNotifySettings(global), selectNotifyExceptions(global)),
lastMessageSender,
actionTargetUserIds,
actionTargetChatId,
actionTargetMessage,
draft: selectDraft(global, chatId, MAIN_THREAD_ID),
animationLevel: global.settings.byKey.animationLevel,
isForumPanelActive,
isSelected,
canScrollDown: isSelected && messageListType === 'thread',
canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1,
lastSyncTime: global.lastSyncTime,
...(isOutgoing && chat.lastMessage && {
lastMessageOutgoingStatus: selectOutgoingStatus(global, chat.lastMessage),
}),
user,
userStatus,
isEmojiStatusColored,
lastMessageTopic,
typingStatus,
forumPanelChatId: global.forumPanelChatId,
};
},
)(Chat));