import React, { FC, useEffect, useState, memo, useMemo, useCallback, } from '../../lib/teact/teact'; import { withGlobal } from '../../lib/teact/teactn'; import { ApiChatBannedRights, MAIN_THREAD_ID } from '../../api/types'; import { GlobalActions, MessageListType, MessageList as GlobalMessageList } from '../../global/types'; import { ThemeKey } from '../../types'; import { MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, MOBILE_SCREEN_MAX_WIDTH, MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN, SAFE_SCREEN_WIDTH_FOR_CHAT_INFO, CONTENT_TYPES_FOR_QUICK_UPLOAD, ANIMATION_LEVEL_MAX, ANIMATION_END_DELAY, DARK_THEME_BG_COLOR, LIGHT_THEME_BG_COLOR, ANIMATION_LEVEL_MIN, } from '../../config'; import { IS_SINGLE_COLUMN_LAYOUT, IS_TABLET_COLUMN_LAYOUT, IS_TOUCH_ENV, MASK_IMAGE_DISABLED, } from '../../util/environment'; import { DropAreaState } from './composer/DropArea'; import { selectChat, selectCurrentMessageList, selectCurrentTextSearch, selectIsChatBotNotStarted, selectIsInSelectMode, selectIsRightColumnShown, selectPinnedIds, selectTheme, } from '../../modules/selectors'; import { getCanPostInChat, getMessageSendingRestrictionReason, isChatPrivate } from '../../modules/helpers'; import captureEscKeyListener from '../../util/captureEscKeyListener'; import { pick } from '../../util/iteratees'; import buildClassName from '../../util/buildClassName'; import useCustomBackground from '../../hooks/useCustomBackground'; import useWindowSize from '../../hooks/useWindowSize'; import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation'; import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms'; import useLang from '../../hooks/useLang'; import useHistoryBack from '../../hooks/useHistoryBack'; import { createMessageHash } from '../../util/routing'; import Transition from '../ui/Transition'; import MiddleHeader from './MiddleHeader'; import MessageList from './MessageList'; import ScrollDownButton from './ScrollDownButton'; import Composer from './composer/Composer'; import Button from '../ui/Button'; import MobileSearch from './MobileSearch.async'; import MessageSelectToolbar from './MessageSelectToolbar.async'; import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async'; import './MiddleColumn.scss'; type StateProps = { chatId?: number; threadId?: number; messageListType?: MessageListType; isPrivate?: boolean; isPinnedMessageList?: boolean; isScheduledMessageList?: boolean; canPost?: boolean; currentUserBannedRights?: ApiChatBannedRights; defaultBannedRights?: ApiChatBannedRights; hasPinnedOrAudioMessage?: boolean; pinnedMessagesCount?: number; theme: ThemeKey; customBackground?: string; backgroundColor?: string; patternColor?: string; isLeftColumnShown?: boolean; isRightColumnShown?: boolean; isBackgroundBlurred?: boolean; isMobileSearchActive?: boolean; isSelectModeActive?: boolean; animationLevel?: number; shouldSkipHistoryAnimations?: boolean; currentTransitionKey: number; messageLists?: GlobalMessageList[]; }; type DispatchProps = Pick; const CLOSE_ANIMATION_DURATION = IS_SINGLE_COLUMN_LAYOUT ? 450 + ANIMATION_END_DELAY : undefined; function canBeQuicklyUploaded(item: DataTransferItem) { return item.kind === 'file' && item.type && CONTENT_TYPES_FOR_QUICK_UPLOAD.has(item.type); } const MiddleColumn: FC = ({ chatId, threadId, messageListType, isPrivate, isPinnedMessageList, messageLists, canPost, currentUserBannedRights, defaultBannedRights, hasPinnedOrAudioMessage, pinnedMessagesCount, customBackground, theme, backgroundColor, patternColor, isLeftColumnShown, isRightColumnShown, isBackgroundBlurred, isMobileSearchActive, isSelectModeActive, animationLevel, shouldSkipHistoryAnimations, currentTransitionKey, openChat, unpinAllMessages, loadUser, closeLocalTextSearch, exitMessageSelectMode, }) => { const { width: windowWidth } = useWindowSize(); const lang = useLang(); const [dropAreaState, setDropAreaState] = useState(DropAreaState.None); const [isFabShown, setIsFabShown] = useState(); const [isNotchShown, setIsNotchShown] = useState(); const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false); const [isReady, setIsReady] = useState(!IS_SINGLE_COLUMN_LAYOUT || animationLevel === ANIMATION_LEVEL_MIN); const hasTools = hasPinnedOrAudioMessage && ( windowWidth < MOBILE_SCREEN_MAX_WIDTH || ( isRightColumnShown && windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN && windowWidth < SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN ) || ( windowWidth >= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN && windowWidth < SAFE_SCREEN_WIDTH_FOR_CHAT_INFO ) ); const renderingChatId = usePrevDuringAnimation(chatId, CLOSE_ANIMATION_DURATION); const renderingThreadId = usePrevDuringAnimation(threadId, CLOSE_ANIMATION_DURATION); const renderingMessageListType = usePrevDuringAnimation(messageListType, CLOSE_ANIMATION_DURATION); const renderingCanPost = usePrevDuringAnimation(canPost, CLOSE_ANIMATION_DURATION); const renderingHasTools = usePrevDuringAnimation(hasTools, CLOSE_ANIMATION_DURATION); const renderingIsFabShown = usePrevDuringAnimation(isFabShown, CLOSE_ANIMATION_DURATION); useEffect(() => { return chatId ? captureEscKeyListener(() => { openChat({ id: undefined }); }) : undefined; }, [chatId, openChat]); useEffect(() => { setDropAreaState(DropAreaState.None); setIsFabShown(undefined); setIsNotchShown(undefined); }, [chatId]); useEffect(() => { if (animationLevel === ANIMATION_LEVEL_MIN) { setIsReady(true); } }, [animationLevel]); const handleTransitionEnd = (e: React.TransitionEvent) => { if (e.propertyName === 'transform' && e.target === e.currentTarget) { setIsReady(Boolean(chatId)); } }; useEffect(() => { if (isPrivate) { loadUser({ userId: chatId }); } }, [chatId, isPrivate, loadUser]); const handleDragEnter = useCallback((e: React.DragEvent) => { if (IS_TOUCH_ENV) { return; } const { items } = e.dataTransfer || {}; const shouldDrawQuick = items && Array.from(items) // Filter unnecessary element for drag and drop images in Firefox (https://github.com/Ajaxy/telegram-tt/issues/49) // https://developer.mozilla.org/en-US/docs/Web/API/HTML_Drag_and_Drop_API/Recommended_drag_types#image .filter((item) => item.type !== 'text/uri-list') .every(canBeQuicklyUploaded); setDropAreaState(shouldDrawQuick ? DropAreaState.QuickFile : DropAreaState.Document); }, []); const handleHideDropArea = useCallback(() => { setDropAreaState(DropAreaState.None); }, []); const handleOpenUnpinModal = useCallback(() => { setIsUnpinModalOpen(true); }, []); const closeUnpinModal = useCallback(() => { setIsUnpinModalOpen(false); }, []); const handleUnpinAllMessages = useCallback(() => { unpinAllMessages({ chatId }); closeUnpinModal(); openChat({ id: chatId }); }, [unpinAllMessages, openChat, closeUnpinModal, chatId]); const handleTabletFocus = useCallback(() => { openChat({ id: chatId }); }, [openChat, chatId]); const customBackgroundValue = useCustomBackground(theme, customBackground); const className = buildClassName( renderingHasTools && 'has-header-tools', customBackground && 'custom-bg-image', backgroundColor && 'custom-bg-color', customBackground && isBackgroundBlurred && 'blurred', MASK_IMAGE_DISABLED ? 'mask-image-disabled' : 'mask-image-enabled', ); const messagingDisabledClassName = buildClassName( 'messaging-disabled', !isSelectModeActive && 'shown', ); const messageSendingRestrictionReason = getMessageSendingRestrictionReason( lang, currentUserBannedRights, defaultBannedRights, ); // CSS Variables calculation doesn't work properly with transforms, so we calculate transform values in JS const { composerHiddenScale, toolbarHiddenScale, composerTranslateX, toolbarTranslateX, unpinHiddenScale, toolbarForUnpinHiddenScale, } = useMemo( () => calculateMiddleFooterTransforms(windowWidth, renderingCanPost), [renderingCanPost, windowWidth], ); const footerClassName = buildClassName( 'middle-column-footer', !renderingCanPost && 'no-composer', renderingCanPost && isNotchShown && !isSelectModeActive && 'with-notch', ); const closeChat = () => { openChat({ id: undefined }, true); }; useHistoryBack(renderingChatId && renderingThreadId, closeChat, undefined, undefined, undefined, messageLists ? messageLists.map(createMessageHash) : []); useHistoryBack(isMobileSearchActive, closeLocalTextSearch); useHistoryBack(isSelectModeActive, exitMessageSelectMode); return (
{renderingChatId && renderingThreadId && ( <>
{() => ( <>
{renderingCanPost && ( )} {isPinnedMessageList && (
)} {!isPinnedMessageList && !renderingCanPost && messageSendingRestrictionReason && (
{messageSendingRestrictionReason}
)}
)}
{IS_SINGLE_COLUMN_LAYOUT && } )} {chatId && ( )}
); }; export default memo(withGlobal( (global): StateProps => { const theme = selectTheme(global); const { isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor, patternColor, } = global.settings.themes[theme] || {}; const { messageLists } = global.messages; const currentMessageList = selectCurrentMessageList(global); const { isLeftColumnShown, chats: { listIds } } = global; const state: StateProps = { theme, customBackground, backgroundColor, patternColor, isLeftColumnShown, isRightColumnShown: selectIsRightColumnShown(global), isBackgroundBlurred, isMobileSearchActive: Boolean(IS_SINGLE_COLUMN_LAYOUT && selectCurrentTextSearch(global)), isSelectModeActive: selectIsInSelectMode(global), animationLevel: global.settings.byKey.animationLevel, currentTransitionKey: Math.max(0, global.messages.messageLists.length - 1), }; if (!currentMessageList || !listIds.active) { return state; } const { chatId, threadId, type: messageListType } = currentMessageList; const chat = selectChat(global, chatId); const pinnedIds = selectPinnedIds(global, chatId); const { chatId: audioChatId, messageId: audioMessageId } = global.audioPlayer; const canPost = chat && getCanPostInChat(chat, threadId); const isBotNotStarted = selectIsChatBotNotStarted(global, chatId); const isPinnedMessageList = messageListType === 'pinned'; const isScheduledMessageList = messageListType === 'scheduled'; return { ...state, chatId, threadId, messageListType, isPrivate: isChatPrivate(chatId), canPost: !isPinnedMessageList && (!chat || canPost) && !isBotNotStarted, isPinnedMessageList, isScheduledMessageList, currentUserBannedRights: chat && chat.currentUserBannedRights, defaultBannedRights: chat && chat.defaultBannedRights, hasPinnedOrAudioMessage: ( threadId !== MAIN_THREAD_ID || Boolean(pinnedIds && pinnedIds.length) || Boolean(audioChatId && audioMessageId) ), pinnedMessagesCount: pinnedIds ? pinnedIds.length : 0, shouldSkipHistoryAnimations: global.shouldSkipHistoryAnimations, messageLists, }; }, (setGlobal, actions): DispatchProps => pick(actions, [ 'openChat', 'unpinAllMessages', 'loadUser', 'closeLocalTextSearch', 'exitMessageSelectMode', ]), )(MiddleColumn));