import type { FC } from '../../lib/teact/teact'; import React, { memo, useEffect, useLayoutEffect, useRef, } from '../../lib/teact/teact'; import { getActions, withGlobal } from '../../global'; import type { ApiChat, ApiMessage, ApiPeer, ApiSticker, ApiTypingStatus, } from '../../api/types'; import type { GlobalState, MessageListType } from '../../global/types'; import type { Signal } from '../../util/signals'; import { MAIN_THREAD_ID } from '../../api/types'; import { StoryViewerOrigin, type ThreadId } from '../../types'; import { EDITABLE_INPUT_CSS_SELECTOR, MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES, MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, MOBILE_SCREEN_MAX_WIDTH, SAFE_SCREEN_WIDTH_FOR_CHAT_INFO, SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, } from '../../config'; import { requestMutation } from '../../lib/fasterdom/fasterdom'; import { getChatTitle, getIsSavedDialog, getSenderTitle, isChatChannel, isChatSuperGroup, isUserId, } from '../../global/helpers'; import { selectAllowedMessageActionsSlow, selectChat, selectChatMessage, selectChatMessages, selectCurrentMiddleSearch, selectForwardedSender, selectIsChatBotNotStarted, selectIsChatWithBot, selectIsChatWithSelf, selectIsInSelectMode, selectIsRightColumnShown, selectIsUserBlocked, selectPinnedIds, selectScheduledIds, selectTabState, selectThreadInfo, selectThreadParam, } from '../../global/selectors'; import buildClassName from '../../util/buildClassName'; import cycleRestrict from '../../util/cycleRestrict'; import { getMessageKey } from '../../util/keys/messageKey'; import useAppLayout from '../../hooks/useAppLayout'; import useConnectionStatus from '../../hooks/useConnectionStatus'; import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; import useDerivedState from '../../hooks/useDerivedState'; import useElectronDrag from '../../hooks/useElectronDrag'; import useEnsureMessage from '../../hooks/useEnsureMessage'; import useLastCallback from '../../hooks/useLastCallback'; import useLongPress from '../../hooks/useLongPress'; import useOldLang from '../../hooks/useOldLang'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated'; import useWindowSize from '../../hooks/window/useWindowSize'; import GroupCallTopPane from '../calls/group/GroupCallTopPane'; import GroupChatInfo from '../common/GroupChatInfo'; import PrivateChatInfo from '../common/PrivateChatInfo'; import UnreadCounter from '../common/UnreadCounter'; import Button from '../ui/Button'; import Transition from '../ui/Transition'; import AudioPlayer from './AudioPlayer'; import ChatReportPanel from './ChatReportPanel'; import HeaderActions from './HeaderActions'; import HeaderPinnedMessage from './HeaderPinnedMessage'; import './MiddleHeader.scss'; const ANIMATION_DURATION = 350; const BACK_BUTTON_INACTIVE_TIME = 450; const EMOJI_STATUS_SIZE = 22; const SEARCH_LONGTAP_THRESHOLD = 500; type OwnProps = { chatId: string; threadId: ThreadId; messageListType: MessageListType; isComments?: boolean; isReady?: boolean; isMobile?: boolean; getCurrentPinnedIndex: Signal; getLoadingPinnedId: Signal; onFocusPinnedMessage: (messageId: number) => void; }; type StateProps = { chat?: ApiChat; pinnedMessageIds?: number[] | number; messagesById?: Record; canUnpin?: boolean; isSavedDialog?: boolean; topMessageSender?: ApiPeer; typingStatus?: ApiTypingStatus; isSelectModeActive?: boolean; isLeftColumnShown?: boolean; isRightColumnShown?: boolean; audioMessage?: ApiMessage; messagesCount?: number; isChatWithSelf?: boolean; hasButtonInHeader?: boolean; shouldSkipHistoryAnimations?: boolean; currentTransitionKey: number; connectionState?: GlobalState['connectionState']; isSyncing?: boolean; isSynced?: boolean; isFetchingDifference?: boolean; emojiStatusSticker?: ApiSticker; isMiddleSearchOpen?: boolean; }; const MiddleHeader: FC = ({ chatId, threadId, messageListType, isReady, isMobile, pinnedMessageIds, messagesById, canUnpin, topMessageSender, typingStatus, isSelectModeActive, isLeftColumnShown, isRightColumnShown, audioMessage, chat, messagesCount, isComments, isChatWithSelf, hasButtonInHeader, shouldSkipHistoryAnimations, currentTransitionKey, connectionState, isSyncing, isSynced, isFetchingDifference, getCurrentPinnedIndex, getLoadingPinnedId, emojiStatusSticker, isSavedDialog, isMiddleSearchOpen, onFocusPinnedMessage, }) => { const { openThreadWithInfo, pinMessage, focusMessage, openChat, openPreviousChat, loadPinnedMessages, toggleLeftColumn, exitMessageSelectMode, openPremiumModal, openThread, openStickerSet, updateMiddleSearch, } = getActions(); const lang = useOldLang(); const isBackButtonActive = useRef(true); const { isTablet } = useAppLayout(); const currentPinnedIndex = useDerivedState(getCurrentPinnedIndex); const pinnedMessageId = Array.isArray(pinnedMessageIds) ? pinnedMessageIds[currentPinnedIndex] : pinnedMessageIds; const pinnedMessage = messagesById && pinnedMessageId ? messagesById[pinnedMessageId] : undefined; const pinnedMessagesCount = Array.isArray(pinnedMessageIds) ? pinnedMessageIds.length : (pinnedMessageIds ? 1 : undefined); const chatTitleLength = chat && getChatTitle(lang, chat).length; const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined; const { settings } = chat || {}; const isForum = chat?.isForum; useEffect(() => { if (isSynced && isReady && (threadId === MAIN_THREAD_ID || isForum)) { loadPinnedMessages({ chatId, threadId }); } }, [chatId, threadId, isSynced, isReady, isForum]); useEnsureMessage(chatId, pinnedMessageId, pinnedMessage); const { width: windowWidth } = useWindowSize(); const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN; const shouldShowCloseButton = isTablet && isLeftColumnShown; // eslint-disable-next-line no-null/no-null const componentRef = useRef(null); const shouldAnimateTools = useRef(true); const handleOpenSearch = useLastCallback(() => { updateMiddleSearch({ chatId, threadId, update: {} }); }); const handleOpenChat = useLastCallback((event: React.MouseEvent | React.TouchEvent) => { if ((event.target as Element).closest('.title > .custom-emoji')) return; openThreadWithInfo({ chatId, threadId }); }); const { onMouseDown: handleLongPressMouseDown, onMouseUp: handleLongPressMouseUp, onMouseLeave: handleLongPressMouseLeave, onTouchStart: handleLongPressTouchStart, onTouchEnd: handleLongPressTouchEnd, } = useLongPress({ onStart: handleOpenSearch, onClick: handleOpenChat, threshold: SEARCH_LONGTAP_THRESHOLD, }); const handleUnpinMessage = useLastCallback((messageId: number) => { pinMessage({ messageId, isUnpin: true }); }); const handlePinnedMessageClick = useLastCallback((e: React.MouseEvent): void => { const messageId = e.shiftKey && Array.isArray(pinnedMessageIds) ? pinnedMessageIds[cycleRestrict(pinnedMessageIds.length, pinnedMessageIds.indexOf(pinnedMessageId!) - 2)] : pinnedMessageId!; if (!getLoadingPinnedId()) { focusMessage({ chatId, threadId, messageId, noForumTopicPanel: true, }); onFocusPinnedMessage(messageId); } }); const handleAllPinnedClick = useLastCallback(() => { openThread({ chatId, threadId, type: 'pinned' }); }); const setBackButtonActive = useLastCallback(() => { setTimeout(() => { isBackButtonActive.current = true; }, BACK_BUTTON_INACTIVE_TIME); }); const handleUserStatusClick = useLastCallback(() => { openPremiumModal({ fromUserId: chatId }); }); const handleChannelStatusClick = useLastCallback(() => { openStickerSet({ stickerSetInfo: emojiStatusSticker!.stickerSetInfo, }); }); const handleBackClick = useLastCallback((e: React.MouseEvent) => { if (!isBackButtonActive.current) return; // Workaround for missing UI when quickly clicking the Back button isBackButtonActive.current = false; if (isMobile) { const messageInput = document.querySelector(EDITABLE_INPUT_CSS_SELECTOR); messageInput?.blur(); } if (isSelectModeActive) { exitMessageSelectMode(); setBackButtonActive(); return; } if (messageListType === 'thread' && currentTransitionKey === 0) { if (!isTablet || shouldShowCloseButton) { e.stopPropagation(); // Stop propagation to prevent chat re-opening on tablets openChat({ id: undefined }, { forceOnHeavyAnimation: true }); } else { toggleLeftColumn(); } setBackButtonActive(); return; } openPreviousChat(); setBackButtonActive(); }); const canToolsCollideWithChatInfo = ( windowWidth >= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN && windowWidth < SAFE_SCREEN_WIDTH_FOR_CHAT_INFO ) || ( windowWidth > MOBILE_SCREEN_MAX_WIDTH && windowWidth < MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN && (!chatTitleLength || chatTitleLength > 30) ); const shouldUseStackedToolsClass = canToolsCollideWithChatInfo || ( windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN && windowWidth < SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN ); const hasChatSettings = Boolean(settings?.canAddContact || settings?.canBlockContact || settings?.canReportSpam); const { shouldRender: shouldShowChatReportPanel, transitionClassNames: chatReportPanelClassNames, } = useShowTransitionDeprecated(hasChatSettings); const renderingChatSettings = useCurrentOrPrev(hasChatSettings ? settings : undefined, true); const { shouldRender: shouldRenderAudioPlayer, transitionClassNames: audioPlayerClassNames, } = useShowTransitionDeprecated(Boolean(audioMessage)); const renderingAudioMessage = useCurrentOrPrev(audioMessage, true); const { shouldRender: shouldRenderPinnedMessage, transitionClassNames: pinnedMessageClassNames, } = useShowTransitionDeprecated(Boolean(pinnedMessage) && !isMiddleSearchOpen, undefined, true); const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true); const renderingPinnedMessagesCount = useCurrentOrPrev(pinnedMessagesCount, true); const renderingCanUnpin = useCurrentOrPrev(canUnpin, true); const renderingPinnedMessageTitle = useCurrentOrPrev(topMessageTitle); const prevTransitionKey = usePreviousDeprecated(currentTransitionKey); const cleanupExceptionKey = ( prevTransitionKey !== undefined && prevTransitionKey < currentTransitionKey ? prevTransitionKey : undefined ); const canRevealTools = (shouldRenderPinnedMessage && renderingPinnedMessage) || (shouldRenderAudioPlayer && renderingAudioMessage); // Logic for transition to and from custom display of AudioPlayer/PinnedMessage on smaller screens useLayoutEffect(() => { const componentEl = componentRef.current; if (!componentEl) { return; } if (!shouldUseStackedToolsClass || !canRevealTools) { componentEl.classList.remove('tools-stacked', 'animated'); shouldAnimateTools.current = true; return; } if (isRightColumnShown || canToolsCollideWithChatInfo) { if (shouldAnimateTools.current) { componentEl.classList.add('tools-stacked', 'animated'); shouldAnimateTools.current = false; } // Remove animation class to prevent it messing up the show transitions setTimeout(() => { requestMutation(() => { componentEl.classList.remove('animated'); }); }, ANIMATION_DURATION); } else { componentEl.classList.remove('tools-stacked'); shouldAnimateTools.current = true; } }, [shouldUseStackedToolsClass, canRevealTools, canToolsCollideWithChatInfo, isRightColumnShown]); const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing || isFetchingDifference, true); function renderInfo() { if (messageListType === 'thread') { if (threadId === MAIN_THREAD_ID || isSavedDialog || chat?.isForum) { return renderChatInfo(); } } return ( <> {renderBackButton()}

{messagesCount !== undefined ? ( messageListType === 'thread' ? ( (messagesCount ? lang(isComments ? 'Comments' : 'Replies', messagesCount, 'i') : lang(isComments ? 'CommentsTitle' : 'RepliesTitle'))) : messageListType === 'pinned' ? (lang('PinnedMessagesCount', messagesCount, 'i')) : messageListType === 'scheduled' ? ( isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i') ) : undefined ) : lang('Loading')}

); } function renderChatInfo() { // TODO Implement count const savedMessagesStatus = isSavedDialog ? lang('SavedMessages') : undefined; const realChatId = isSavedDialog ? String(threadId) : chatId; return ( <> {(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, !isSavedDialog)}
{isUserId(realChatId) ? ( ) : ( )}
); } function renderBackButton(asClose = false, withUnreadCounter = false) { return (
{withUnreadCounter && }
); } const isAudioPlayerRendered = Boolean(shouldRenderAudioPlayer && renderingAudioMessage); const isPinnedMessagesFullWidth = isAudioPlayerRendered || (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES); useElectronDrag(componentRef); return (
{renderInfo()} {threadId === MAIN_THREAD_ID && !chat?.isForum && ( )} {shouldRenderPinnedMessage && renderingPinnedMessage && ( )} {shouldShowChatReportPanel && ( )}
{isAudioPlayerRendered && ( )}
); }; export default memo(withGlobal( (global, { chatId, threadId, messageListType, isMobile, }): StateProps => { const { isLeftColumnShown, shouldSkipHistoryAnimations, audioPlayer, messageLists, } = selectTabState(global); const chat = selectChat(global, chatId); const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer; const audioMessage = audioChatId && audioMessageId ? selectChatMessage(global, audioChatId, audioMessageId) : undefined; let messagesCount: number | undefined; if (messageListType === 'pinned') { const pinnedIds = selectPinnedIds(global, chatId, threadId); messagesCount = pinnedIds?.length; } else if (messageListType === 'scheduled') { const scheduledIds = selectScheduledIds(global, chatId, threadId); messagesCount = scheduledIds?.length; } else if (messageListType === 'thread' && threadId !== MAIN_THREAD_ID) { const threadInfo = selectThreadInfo(global, chatId, threadId); messagesCount = threadInfo?.messagesCount || 0; } const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID; const isChatWithBot = chat && selectIsChatWithBot(global, chat); const canRestartBot = Boolean(isChatWithBot && selectIsUserBlocked(global, chatId)); const canStartBot = isChatWithBot && !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId)); const canSubscribe = Boolean( chat && (isMainThread || chat.isForum) && (isChatChannel(chat) || isChatSuperGroup(chat)) && chat.isNotJoined, ); const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest); const typingStatus = selectThreadParam(global, chatId, threadId, 'typingStatus'); const emojiStatus = chat?.emojiStatus; const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId]; const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const isMiddleSearchOpen = Boolean(selectCurrentMiddleSearch(global)); const state: StateProps = { typingStatus, isLeftColumnShown, isRightColumnShown: selectIsRightColumnShown(global, isMobile), isSelectModeActive: selectIsInSelectMode(global), audioMessage, chat, messagesCount, isChatWithSelf: selectIsChatWithSelf(global, chatId), shouldSkipHistoryAnimations, currentTransitionKey: Math.max(0, messageLists.length - 1), connectionState: global.connectionState, isSyncing: global.isSyncing, isSynced: global.isSynced, isFetchingDifference: global.isFetchingDifference, emojiStatusSticker, hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest, isSavedDialog, isMiddleSearchOpen, }; const messagesById = selectChatMessages(global, chatId); if (messageListType !== 'thread' || !messagesById) { return state; } if (threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum) { const pinnedMessageId = Number(threadId); const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined; const topMessageSender = message ? selectForwardedSender(global, message) : undefined; return { ...state, pinnedMessageIds: pinnedMessageId, messagesById, canUnpin: false, topMessageSender, }; } const pinnedMessageIds = !isSavedDialog ? selectPinnedIds(global, chatId, threadId) : undefined; if (pinnedMessageIds?.length) { const firstPinnedMessage = messagesById[pinnedMessageIds[0]]; const { canUnpin = false, } = ( firstPinnedMessage && pinnedMessageIds.length === 1 && selectAllowedMessageActionsSlow(global, firstPinnedMessage, threadId) ) || {}; return { ...state, pinnedMessageIds, messagesById, canUnpin, }; } return state; }, )(MiddleHeader));