412 lines
13 KiB
TypeScript
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));
|