import type React from '../../../lib/teact/teact'; import { memo, useEffect } from '../../../lib/teact/teact'; import { getActions, withGlobal } from '../../../global'; import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types'; import type { MessageListType, ThreadId } from '../../../types'; import type { Signal } from '../../../util/signals'; import { MAIN_THREAD_ID } from '../../../api/types'; import { getIsSavedDialog, getMessageIsSpoiler, getMessageSingleInlineButton, } from '../../../global/helpers'; import { getPeerTitle } from '../../../global/helpers/peers'; import { selectAllowedMessageActionsSlow, selectChat, selectChatMessage, selectChatMessages, selectForwardedSender, selectPinnedIds, } from '../../../global/selectors'; import { IS_TOUCH_ENV } from '../../../util/browser/windowEnvironment'; import buildClassName from '../../../util/buildClassName'; import cycleRestrict from '../../../util/cycleRestrict'; import { REM } from '../../common/helpers/mediaDimensions'; import renderText from '../../common/helpers/renderText'; import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev'; import useDerivedState from '../../../hooks/useDerivedState'; import useEnsureMessage from '../../../hooks/useEnsureMessage'; import { useFastClick } from '../../../hooks/useFastClick'; import useFlag from '../../../hooks/useFlag'; import useLang from '../../../hooks/useLang'; import useLastCallback from '../../../hooks/useLastCallback'; import useShowTransition from '../../../hooks/useShowTransition'; import useAsyncRendering from '../../right/hooks/useAsyncRendering'; import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane'; import AnimatedCounter from '../../common/AnimatedCounter'; import CompactMediaPreview, { canRenderCompactMediaPreview } from '../../common/CompactMediaPreview'; import Icon from '../../common/icons/Icon'; import MessageSummary from '../../common/MessageSummary'; import Button from '../../ui/Button'; import ConfirmDialog from '../../ui/ConfirmDialog'; import RippleEffect from '../../ui/RippleEffect'; import Spinner from '../../ui/Spinner'; import Transition from '../../ui/Transition'; import PinnedMessageNavigation from '../PinnedMessageNavigation'; import styles from './HeaderPinnedMessage.module.scss'; const MAX_LENGTH = 256; const SHOW_LOADER_DELAY = 450; const EMOJI_SIZE = 1.125 * REM; type OwnProps = { chatId: string; threadId: ThreadId; messageListType: MessageListType; className?: string; isFullWidth?: boolean; shouldHide?: boolean; getLoadingPinnedId: Signal; getCurrentPinnedIndex: Signal; onFocusPinnedMessage: (messageId: number) => void; onPaneStateChange?: (state: PaneState) => void; }; type StateProps = { chat?: ApiChat; pinnedMessageIds?: number[] | number; messagesById?: Record; canUnpin?: boolean; topMessageSender?: ApiPeer; isSynced?: boolean; }; const HeaderPinnedMessage = ({ chatId, threadId, canUnpin, getLoadingPinnedId, pinnedMessageIds, messagesById, isFullWidth, topMessageSender, getCurrentPinnedIndex, className, chat, isSynced, shouldHide, onPaneStateChange, onFocusPinnedMessage, }: OwnProps & StateProps) => { const { clickBotInlineButton, focusMessage, openThread, pinMessage, loadPinnedMessages, } = getActions(); const lang = useLang(); 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 : 0); const pinnedMessageNumber = Math.max(pinnedMessagesCount - currentPinnedIndex, 1); const topMessageTitle = topMessageSender ? getPeerTitle(lang, topMessageSender) : undefined; const isLoading = Boolean(useDerivedState(getLoadingPinnedId)); const canRenderLoader = useAsyncRendering([isLoading], SHOW_LOADER_DELAY); const shouldShowLoader = canRenderLoader && isLoading; const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true); const hasPictogram = Boolean( renderingPinnedMessage && canRenderCompactMediaPreview(renderingPinnedMessage.content), ); const isSpoiler = renderingPinnedMessage && getMessageIsSpoiler(renderingPinnedMessage); useEffect(() => { if (isSynced && (threadId === MAIN_THREAD_ID || chat?.isForum)) { loadPinnedMessages({ chatId, threadId }); } }, [chatId, threadId, isSynced, chat?.isForum]); useEnsureMessage(chatId, pinnedMessageId, pinnedMessage); const isOpen = Boolean(pinnedMessage) && !shouldHide; const { ref: transitionRef, } = useShowTransition({ isOpen, noOpenTransition: true, shouldForceOpen: isFullWidth, // Use pane animation instead }); const { ref, shouldRender } = useHeaderPane({ isOpen, isDisabled: !isFullWidth, ref: transitionRef, onStateChange: onPaneStateChange, }); const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag(); const handleUnpinMessage = useLastCallback(() => { closeUnpinDialog(); pinMessage({ chatId, messageId: pinnedMessage!.id, isUnpin: true }); }); const inlineButton = pinnedMessage && getMessageSingleInlineButton(pinnedMessage); const handleInlineButtonClick = useLastCallback(() => { if (inlineButton) { clickBotInlineButton({ chatId: pinnedMessage.chatId, messageId: pinnedMessage.id, button: inlineButton }); } }); const handleAllPinnedClick = useLastCallback(() => { openThread({ chatId, threadId, type: 'pinned' }); }); const handleMessageClick = useLastCallback((e: React.MouseEvent): void => { const nextMessageId = e.shiftKey && Array.isArray(pinnedMessageIds) ? pinnedMessageIds[cycleRestrict(pinnedMessageIds.length, pinnedMessageIds.indexOf(pinnedMessageId!) - 2)] : pinnedMessageId!; if (!getLoadingPinnedId()) { focusMessage({ chatId, threadId, messageId: nextMessageId, noForumTopicPanel: true, }); onFocusPinnedMessage(nextMessageId); } }); const [noHoverColor, markNoHoverColor, unmarkNoHoverColor] = useFlag(); const { handleClick, handleMouseDown } = useFastClick(handleMessageClick); if (!shouldRender || !renderingPinnedMessage) return undefined; return (
{(pinnedMessagesCount > 1 || shouldShowLoader) && ( )} {canUnpin && ( )}
); }; export default memo(withGlobal( (global, { chatId, threadId, messageListType, }): Complete => { const chat = selectChat(global, chatId); const isSynced = global.isSynced; const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const messagesById = selectChatMessages(global, chatId); const state = { chat, isSynced, }; if (messageListType !== 'thread' || !messagesById) { return state as Complete; } 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, } as Complete; } return state as Complete; }, )(HeaderPinnedMessage));