287 lines
8.6 KiB
TypeScript
287 lines
8.6 KiB
TypeScript
import type { FC } from '../../../lib/teact/teact';
|
|
import React, { memo } from '../../../lib/teact/teact';
|
|
import { getActions, withGlobal } from '../../../global';
|
|
|
|
import type {
|
|
ApiChat, ApiDraft, ApiMessage, ApiMessageOutgoingStatus,
|
|
ApiPeer, ApiTopic, ApiTypeStory, ApiTypingStatus,
|
|
} from '../../../api/types';
|
|
import type { ObserveFn } from '../../../hooks/useIntersectionObserver';
|
|
import type { ChatAnimationTypes } from './hooks';
|
|
|
|
import { getMessageAction, groupStatetefulContent } from '../../../global/helpers';
|
|
import { getMessageReplyInfo } from '../../../global/helpers/replies';
|
|
import {
|
|
selectCanAnimateInterface,
|
|
selectCanDeleteTopic,
|
|
selectChat,
|
|
selectChatMessage,
|
|
selectCurrentMessageList,
|
|
selectDraft,
|
|
selectOutgoingStatus,
|
|
selectPeerStory,
|
|
selectSender,
|
|
selectThreadInfo,
|
|
selectThreadParam,
|
|
selectTopics,
|
|
} from '../../../global/selectors';
|
|
import buildClassName from '../../../util/buildClassName';
|
|
import { createLocationHash } from '../../../util/routing';
|
|
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../util/windowEnvironment';
|
|
import renderText from '../../common/helpers/renderText';
|
|
|
|
import useFlag from '../../../hooks/useFlag';
|
|
import useLastCallback from '../../../hooks/useLastCallback';
|
|
import useOldLang from '../../../hooks/useOldLang';
|
|
import useChatListEntry from './hooks/useChatListEntry';
|
|
import useTopicContextActions from './hooks/useTopicContextActions';
|
|
|
|
import Icon from '../../common/icons/Icon';
|
|
import LastMessageMeta from '../../common/LastMessageMeta';
|
|
import TopicIcon from '../../common/TopicIcon';
|
|
import ConfirmDialog from '../../ui/ConfirmDialog';
|
|
import ListItem from '../../ui/ListItem';
|
|
import MuteChatModal from '../MuteChatModal.async';
|
|
import ChatBadge from './ChatBadge';
|
|
|
|
import styles from './Topic.module.scss';
|
|
|
|
type OwnProps = {
|
|
chatId: string;
|
|
topic: ApiTopic;
|
|
isSelected: boolean;
|
|
style: string;
|
|
observeIntersection?: ObserveFn;
|
|
orderDiff: number;
|
|
animationType: ChatAnimationTypes;
|
|
};
|
|
|
|
type StateProps = {
|
|
chat: ApiChat;
|
|
canDelete?: boolean;
|
|
lastMessage?: ApiMessage;
|
|
lastMessageStory?: ApiTypeStory;
|
|
lastMessageOutgoingStatus?: ApiMessageOutgoingStatus;
|
|
actionTargetMessage?: ApiMessage;
|
|
actionTargetUserIds?: string[];
|
|
lastMessageSender?: ApiPeer;
|
|
actionTargetChatId?: string;
|
|
typingStatus?: ApiTypingStatus;
|
|
draft?: ApiDraft;
|
|
canScrollDown?: boolean;
|
|
wasTopicOpened?: boolean;
|
|
withInterfaceAnimations?: boolean;
|
|
topics?: Record<number, ApiTopic>;
|
|
};
|
|
|
|
const Topic: FC<OwnProps & StateProps> = ({
|
|
topic,
|
|
isSelected,
|
|
chatId,
|
|
chat,
|
|
style,
|
|
lastMessage,
|
|
lastMessageStory,
|
|
canScrollDown,
|
|
lastMessageOutgoingStatus,
|
|
observeIntersection,
|
|
canDelete,
|
|
actionTargetMessage,
|
|
actionTargetUserIds,
|
|
actionTargetChatId,
|
|
lastMessageSender,
|
|
animationType,
|
|
withInterfaceAnimations,
|
|
orderDiff,
|
|
typingStatus,
|
|
draft,
|
|
wasTopicOpened,
|
|
topics,
|
|
}) => {
|
|
const {
|
|
openThread,
|
|
deleteTopic,
|
|
focusLastMessage,
|
|
setViewForumAsMessages,
|
|
} = getActions();
|
|
|
|
const lang = useOldLang();
|
|
|
|
const [isDeleteModalOpen, openDeleteModal, closeDeleteModal] = useFlag();
|
|
const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag();
|
|
const [shouldRenderDeleteModal, markRenderDeleteModal, unmarkRenderDeleteModal] = useFlag();
|
|
const [shouldRenderMuteModal, markRenderMuteModal, unmarkRenderMuteModal] = useFlag();
|
|
|
|
const {
|
|
isPinned, isClosed,
|
|
} = topic;
|
|
const isMuted = topic.isMuted || (topic.isMuted === undefined && chat.isMuted);
|
|
|
|
const handleOpenDeleteModal = useLastCallback(() => {
|
|
markRenderDeleteModal();
|
|
openDeleteModal();
|
|
});
|
|
|
|
const handleDelete = useLastCallback(() => {
|
|
deleteTopic({ chatId: chat.id, topicId: topic.id });
|
|
});
|
|
|
|
const handleMute = useLastCallback(() => {
|
|
markRenderMuteModal();
|
|
openMuteModal();
|
|
});
|
|
|
|
const { renderSubtitle, ref } = useChatListEntry({
|
|
chat,
|
|
chatId,
|
|
lastMessage,
|
|
draft,
|
|
actionTargetMessage,
|
|
actionTargetUserIds,
|
|
actionTargetChatId,
|
|
lastMessageSender,
|
|
lastMessageTopic: topic,
|
|
observeIntersection,
|
|
isTopic: true,
|
|
typingStatus,
|
|
topics,
|
|
statefulMediaContent: groupStatetefulContent({ story: lastMessageStory }),
|
|
|
|
animationType,
|
|
withInterfaceAnimations,
|
|
orderDiff,
|
|
});
|
|
|
|
const handleOpenTopic = useLastCallback(() => {
|
|
openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true });
|
|
setViewForumAsMessages({ chatId, isEnabled: false });
|
|
|
|
if (canScrollDown) {
|
|
focusLastMessage();
|
|
}
|
|
});
|
|
|
|
const contextActions = useTopicContextActions({
|
|
topic,
|
|
chat,
|
|
wasOpened: wasTopicOpened,
|
|
canDelete,
|
|
handleDelete: handleOpenDeleteModal,
|
|
handleMute,
|
|
});
|
|
|
|
return (
|
|
<ListItem
|
|
className={buildClassName(
|
|
styles.root,
|
|
'Chat',
|
|
isSelected && 'selected',
|
|
'chat-item-clickable',
|
|
)}
|
|
onClick={handleOpenTopic}
|
|
style={style}
|
|
href={IS_OPEN_IN_NEW_TAB_SUPPORTED ? `#${createLocationHash(chatId, 'thread', topic.id)}` : undefined}
|
|
contextActions={contextActions}
|
|
withPortalForMenu
|
|
ref={ref}
|
|
>
|
|
<div className="info">
|
|
<div className="info-row">
|
|
<div className={buildClassName('title')}>
|
|
<TopicIcon topic={topic} className={styles.topicIcon} observeIntersection={observeIntersection} />
|
|
<h3 dir="auto" className="fullName">{renderText(topic.title)}</h3>
|
|
</div>
|
|
{topic.isMuted && <Icon name="muted" />}
|
|
<div className="separator" />
|
|
{isClosed && (
|
|
<Icon name="lock-badge" className={styles.closedIcon} />
|
|
)}
|
|
{lastMessage && (
|
|
<LastMessageMeta
|
|
message={lastMessage}
|
|
outgoingStatus={lastMessageOutgoingStatus}
|
|
/>
|
|
)}
|
|
</div>
|
|
<div className="subtitle">
|
|
{renderSubtitle()}
|
|
<ChatBadge
|
|
chat={chat}
|
|
isPinned={isPinned}
|
|
isMuted={isMuted}
|
|
topic={topic}
|
|
wasTopicOpened={wasTopicOpened}
|
|
topics={topics}
|
|
/>
|
|
</div>
|
|
</div>
|
|
{shouldRenderDeleteModal && (
|
|
<ConfirmDialog
|
|
isOpen={isDeleteModalOpen}
|
|
onClose={closeDeleteModal}
|
|
onCloseAnimationEnd={unmarkRenderDeleteModal}
|
|
confirmIsDestructive
|
|
confirmHandler={handleDelete}
|
|
text={lang('lng_forum_topic_delete_sure')}
|
|
confirmLabel={lang('Delete')}
|
|
/>
|
|
)}
|
|
{shouldRenderMuteModal && (
|
|
<MuteChatModal
|
|
isOpen={isMuteModalOpen}
|
|
onClose={closeMuteModal}
|
|
onCloseAnimationEnd={unmarkRenderMuteModal}
|
|
chatId={chatId}
|
|
topicId={topic.id}
|
|
/>
|
|
)}
|
|
</ListItem>
|
|
);
|
|
};
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { chatId, topic, isSelected }) => {
|
|
const chat = selectChat(global, chatId);
|
|
|
|
const lastMessage = selectChatMessage(global, chatId, topic.lastMessageId);
|
|
const { isOutgoing } = lastMessage || {};
|
|
const replyToMessageId = lastMessage && getMessageReplyInfo(lastMessage)?.replyToMsgId;
|
|
const lastMessageSender = lastMessage && selectSender(global, lastMessage);
|
|
const lastMessageAction = lastMessage ? getMessageAction(lastMessage) : undefined;
|
|
const actionTargetMessage = lastMessageAction && replyToMessageId
|
|
? selectChatMessage(global, chatId, replyToMessageId)
|
|
: undefined;
|
|
const { targetUserIds: actionTargetUserIds, targetChatId: actionTargetChatId } = lastMessageAction || {};
|
|
const typingStatus = selectThreadParam(global, chatId, topic.id, 'typingStatus');
|
|
const draft = selectDraft(global, chatId, topic.id);
|
|
const threadInfo = selectThreadInfo(global, chatId, topic.id);
|
|
const wasTopicOpened = Boolean(threadInfo?.lastReadInboxMessageId);
|
|
const topics = selectTopics(global, chatId);
|
|
|
|
const { chatId: currentChatId, threadId: currentThreadId } = selectCurrentMessageList(global) || {};
|
|
|
|
const storyData = lastMessage?.content.storyData;
|
|
const lastMessageStory = storyData && selectPeerStory(global, storyData.peerId, storyData.id);
|
|
|
|
return {
|
|
chat,
|
|
lastMessage,
|
|
actionTargetUserIds,
|
|
actionTargetChatId,
|
|
actionTargetMessage,
|
|
lastMessageSender,
|
|
typingStatus,
|
|
canDelete: selectCanDeleteTopic(global, chatId, topic.id),
|
|
withInterfaceAnimations: selectCanAnimateInterface(global),
|
|
draft,
|
|
...(isOutgoing && lastMessage && {
|
|
lastMessageOutgoingStatus: selectOutgoingStatus(global, lastMessage),
|
|
}),
|
|
canScrollDown: isSelected && chat?.id === currentChatId && currentThreadId === topic.id,
|
|
wasTopicOpened,
|
|
topics,
|
|
lastMessageStory,
|
|
};
|
|
},
|
|
)(Topic));
|