489 lines
14 KiB
TypeScript

import type { FC } from '../../../lib/teact/teact';
import React, { memo, useEffect, useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiChat,
ApiMessage,
ApiMessageOutgoingStatus,
ApiPeer,
ApiTopic,
ApiTypingStatus,
ApiUser,
ApiUserStatus,
} from '../../../api/types';
import type { ApiDraft } from '../../../global/types';
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
import type { ChatAnimationTypes } from './hooks';
import { MAIN_THREAD_ID } from '../../../api/types';
import { StoryViewerOrigin } from '../../../types';
import {
getMessageAction,
getPrivateChatUserId,
isUserId,
isUserOnline,
selectIsChatMuted,
} from '../../../global/helpers';
import { getMessageReplyInfo } from '../../../global/helpers/replies';
import {
selectCanAnimateInterface,
selectChat,
selectChatLastMessage,
selectChatLastMessageId,
selectChatMessage,
selectCurrentMessageList,
selectDraft,
selectIsForumPanelClosed,
selectIsForumPanelOpen,
selectNotifyExceptions,
selectNotifySettings,
selectOutgoingStatus,
selectPeer,
selectTabState,
selectThreadParam,
selectTopicFromMessage,
selectUser,
selectUserStatus,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import { createLocationHash } from '../../../util/routing';
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment';
import useSelectorSignal from '../../../hooks/data/useSelectorSignal';
import useAppLayout from '../../../hooks/useAppLayout';
import useChatContextActions from '../../../hooks/useChatContextActions';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import useFlag from '../../../hooks/useFlag';
import { useIsIntersecting } from '../../../hooks/useIntersectionObserver';
import useLastCallback from '../../../hooks/useLastCallback';
import useShowTransition from '../../../hooks/useShowTransition';
import useChatListEntry from './hooks/useChatListEntry';
import Avatar from '../../common/Avatar';
import DeleteChatModal from '../../common/DeleteChatModal';
import FullNameTitle from '../../common/FullNameTitle';
import LastMessageMeta from '../../common/LastMessageMeta';
import ReportModal from '../../common/ReportModal';
import ListItem from '../../ui/ListItem';
import ChatFolderModal from '../ChatFolderModal.async';
import MuteChatModal from '../MuteChatModal.async';
import ChatBadge from './ChatBadge';
import ChatCallStatus from './ChatCallStatus';
import './Chat.scss';
type OwnProps = {
chatId: string;
folderId?: number;
orderDiff: number;
animationType: ChatAnimationTypes;
isPinned?: boolean;
offsetTop?: number;
isSavedDialog?: boolean;
isPreview?: boolean;
previewMessageId?: number;
className?: string;
observeIntersection?: ObserveFn;
onDragEnter?: (chatId: string) => void;
};
type StateProps = {
chat?: ApiChat;
isMuted?: boolean;
user?: ApiUser;
userStatus?: ApiUserStatus;
actionTargetUserIds?: string[];
actionTargetMessage?: ApiMessage;
actionTargetChatId?: string;
lastMessageSender?: ApiPeer;
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
draft?: ApiDraft;
isSelected?: boolean;
isSelectedForum?: boolean;
isForumPanelOpen?: boolean;
canScrollDown?: boolean;
canChangeFolder?: boolean;
lastMessageTopic?: ApiTopic;
typingStatus?: ApiTypingStatus;
withInterfaceAnimations?: boolean;
lastMessageId?: number;
lastMessage?: ApiMessage;
currentUserId: string;
};
const Chat: FC<OwnProps & StateProps> = ({
chatId,
folderId,
orderDiff,
animationType,
isPinned,
observeIntersection,
chat,
isMuted,
user,
userStatus,
actionTargetUserIds,
lastMessageSender,
lastMessageOutgoingStatus,
actionTargetMessage,
actionTargetChatId,
offsetTop,
draft,
withInterfaceAnimations,
isSelected,
isSelectedForum,
isForumPanelOpen,
canScrollDown,
canChangeFolder,
lastMessageTopic,
typingStatus,
lastMessageId,
lastMessage,
isSavedDialog,
currentUserId,
isPreview,
previewMessageId,
className,
onDragEnter,
}) => {
const {
openChat,
openSavedDialog,
toggleChatInfo,
focusLastMessage,
focusMessage,
loadTopics,
openForumPanel,
closeForumPanel,
setShouldCloseRightColumn,
} = getActions();
const { isMobile } = useAppLayout();
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag();
const [isChatFolderModalOpen, openChatFolderModal, closeChatFolderModal] = useFlag();
const [isReportModalOpen, openReportModal, closeReportModal] = useFlag();
const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag();
const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag();
const [shouldRenderChatFolderModal, markRenderChatFolderModal, unmarkRenderChatFolderModal] = useFlag();
const [shouldRenderReportModal, markRenderReportModal, unmarkRenderReportModal] = useFlag();
const { isForum, isForumAsMessages } = chat || {};
useEnsureMessage(isSavedDialog ? currentUserId : chatId, lastMessageId, lastMessage);
const { renderSubtitle, ref } = useChatListEntry({
chat,
chatId,
lastMessage,
typingStatus,
draft,
actionTargetMessage,
actionTargetUserIds,
actionTargetChatId,
lastMessageTopic,
lastMessageSender,
observeIntersection,
animationType,
withInterfaceAnimations,
orderDiff,
isSavedDialog,
isPreview,
});
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
const handleClick = useLastCallback(() => {
const noForumTopicPanel = isMobile && isForumAsMessages;
if (isMobile) {
setShouldCloseRightColumn({ value: true });
}
if (isPreview) {
focusMessage({
chatId,
messageId: previewMessageId!,
});
return;
}
if (isSavedDialog) {
openSavedDialog({ chatId, noForumTopicPanel: true }, { forceOnHeavyAnimation: true });
if (isMobile) {
toggleChatInfo({ force: false });
}
return;
}
if (isForum) {
if (isForumPanelOpen) {
closeForumPanel(undefined, { forceOnHeavyAnimation: true });
return;
} else {
if (!noForumTopicPanel) {
openForumPanel({ chatId }, { forceOnHeavyAnimation: true });
}
if (!isForumAsMessages) return;
}
}
openChat({ id: chatId, noForumTopicPanel, shouldReplaceHistory: true }, { forceOnHeavyAnimation: true });
if (isSelected && canScrollDown) {
focusLastMessage();
}
});
const handleDragEnter = useLastCallback((e) => {
e.preventDefault();
onDragEnter?.(chatId);
});
const handleDelete = useLastCallback(() => {
markRenderDeleteModal();
openDeleteModal();
});
const handleMute = useLastCallback(() => {
markRenderMuteModal();
openMuteModal();
});
const handleChatFolderChange = useLastCallback(() => {
markRenderChatFolderModal();
openChatFolderModal();
});
const handleReport = useLastCallback(() => {
markRenderReportModal();
openReportModal();
});
const contextActions = useChatContextActions({
chat,
user,
handleDelete,
handleMute,
handleChatFolderChange,
handleReport,
folderId,
isPinned,
isMuted,
canChangeFolder,
isSavedDialog,
currentUserId,
isPreview,
});
const isIntersecting = useIsIntersecting(ref, chat ? observeIntersection : undefined);
// Load the forum topics to display unread count badge
useEffect(() => {
if (isIntersecting && isForum && chat && chat.listedTopicIds === undefined) {
loadTopics({ chatId });
}
}, [chat, chatId, isForum, isIntersecting]);
const isOnline = user && userStatus && isUserOnline(user, userStatus);
const { hasShownClass: isAvatarOnlineShown } = useShowTransition(isOnline);
const href = useMemo(() => {
if (!IS_OPEN_IN_NEW_TAB_SUPPORTED) return undefined;
if (isSavedDialog) {
return `#${createLocationHash(currentUserId, 'thread', chatId)}`;
}
return `#${createLocationHash(chatId, 'thread', MAIN_THREAD_ID)}`;
}, [chatId, currentUserId, isSavedDialog]);
if (!chat) {
return undefined;
}
const peer = user || chat;
const chatClassName = buildClassName(
'Chat chat-item-clickable',
isUserId(chatId) ? 'private' : 'group',
isForum && 'forum',
isSelected && 'selected',
isSelectedForum && 'selected-forum',
isPreview && 'standalone',
className,
);
return (
<ListItem
ref={ref}
className={chatClassName}
href={href}
style={`top: ${offsetTop}px`}
ripple={!isForum && !isMobile}
contextActions={contextActions}
onClick={handleClick}
onDragEnter={handleDragEnter}
withPortalForMenu
>
<div className={buildClassName('status', 'status-clickable')}>
<Avatar
peer={peer}
isSavedMessages={user?.isSelf}
isSavedDialog={isSavedDialog}
withStory={!user?.isSelf}
withStoryGap={isAvatarOnlineShown}
storyViewerOrigin={StoryViewerOrigin.ChatList}
storyViewerMode="single-peer"
/>
<div className="avatar-badge-wrapper">
<div className={buildClassName('avatar-online', isAvatarOnlineShown && 'avatar-online-shown')} />
<ChatBadge chat={chat} isMuted={isMuted} shouldShowOnlyMostImportant forceHidden={getIsForumPanelClosed} />
</div>
{chat.isCallActive && chat.isCallNotEmpty && (
<ChatCallStatus isMobile={isMobile} isSelected={isSelected} isActive={withInterfaceAnimations} />
)}
</div>
<div className="info">
<div className="info-row">
<FullNameTitle
peer={peer}
withEmojiStatus
isSavedMessages={chatId === user?.id && user?.isSelf}
isSavedDialog={isSavedDialog}
observeIntersection={observeIntersection}
/>
{isMuted && !isSavedDialog && <i className="icon icon-muted" />}
<div className="separator" />
{lastMessage && (
<LastMessageMeta
message={lastMessage}
outgoingStatus={!isSavedDialog ? lastMessageOutgoingStatus : undefined}
draftDate={draft?.date}
/>
)}
</div>
<div className="subtitle">
{renderSubtitle()}
{!isPreview && (
<ChatBadge
chat={chat}
isPinned={isPinned}
isMuted={isMuted}
isSavedDialog={isSavedDialog}
/>
)}
</div>
</div>
{shouldRenderDeleteModal && (
<DeleteChatModal
isOpen={isDeleteModalOpen}
onClose={closeDeleteModal}
onCloseAnimationEnd={unmarkRenderDeleteModal}
chat={chat}
isSavedDialog={isSavedDialog}
/>
)}
{shouldRenderMuteModal && (
<MuteChatModal
isOpen={isMuteModalOpen}
onClose={closeMuteModal}
onCloseAnimationEnd={unmarkRenderMuteModal}
chatId={chatId}
/>
)}
{shouldRenderChatFolderModal && (
<ChatFolderModal
isOpen={isChatFolderModalOpen}
onClose={closeChatFolderModal}
onCloseAnimationEnd={unmarkRenderChatFolderModal}
chatId={chatId}
/>
)}
{shouldRenderReportModal && (
<ReportModal
isOpen={isReportModalOpen}
onClose={closeReportModal}
onCloseAnimationEnd={unmarkRenderReportModal}
peerId={chatId}
subject="peer"
/>
)}
</ListItem>
);
};
export default memo(withGlobal<OwnProps>(
(global, {
chatId, isSavedDialog, isPreview, previewMessageId,
}): StateProps => {
const chat = selectChat(global, chatId);
if (!chat) {
return {
currentUserId: global.currentUserId!,
};
}
const lastMessageId = previewMessageId || selectChatLastMessageId(global, chatId, isSavedDialog ? 'saved' : 'all');
const lastMessage = previewMessageId
? selectChatMessage(global, chatId, previewMessageId)
: selectChatLastMessage(global, chatId, isSavedDialog ? 'saved' : 'all');
const { senderId, isOutgoing, forwardInfo } = lastMessage || {};
const actualSenderId = isSavedDialog ? forwardInfo?.fromId : senderId;
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
const lastMessageSender = actualSenderId ? selectPeer(global, actualSenderId) : undefined;
const lastMessageAction = lastMessage ? getMessageAction(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 isSelected = !isPreview && chatId === currentChatId && (isSavedDialog
? chatId === currentThreadId : currentThreadId === MAIN_THREAD_ID);
const isSelectedForum = (chat.isForum && chatId === currentChatId)
|| chatId === selectTabState(global).forumPanelChatId;
const user = privateChatUserId ? selectUser(global, privateChatUserId) : undefined;
const userStatus = privateChatUserId ? selectUserStatus(global, privateChatUserId) : undefined;
const lastMessageTopic = lastMessage && selectTopicFromMessage(global, 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),
isSelected,
isSelectedForum,
isForumPanelOpen: selectIsForumPanelOpen(global),
canScrollDown: isSelected && messageListType === 'thread',
canChangeFolder: (global.chatFolders.orderedIds?.length || 0) > 1,
...(isOutgoing && lastMessage && {
lastMessageOutgoingStatus: selectOutgoingStatus(global, lastMessage),
}),
user,
userStatus,
lastMessageTopic,
typingStatus,
withInterfaceAnimations: selectCanAnimateInterface(global),
lastMessage,
lastMessageId,
currentUserId: global.currentUserId!,
};
},
)(Chat));