TelegramPWA/src/components/middle/MiddleColumn.tsx
2025-06-18 17:43:33 +02:00

931 lines
33 KiB
TypeScript

import type {
ElementRef } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import {
memo, useEffect, useMemo,
useState,
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type {
ApiChat, ApiChatBannedRights, ApiInputMessageReplyInfo, ApiTopic,
} from '../../api/types';
import type {
ActiveEmojiInteraction,
MessageListType,
ThemeKey,
ThreadId,
} from '../../types';
import { MAIN_THREAD_ID } from '../../api/types';
import {
ANIMATION_END_DELAY,
ANONYMOUS_USER_ID,
EDITABLE_INPUT_CSS_SELECTOR,
EDITABLE_INPUT_ID,
GENERAL_TOPIC_ID,
SUPPORTED_PHOTO_CONTENT_TYPES,
SUPPORTED_VIDEO_CONTENT_TYPES,
TMP_CHAT_ID,
} from '../../config';
import { requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom';
import {
getCanPostInChat,
getForumComposerPlaceholder,
getHasAdminRight,
getIsSavedDialog,
getMessageSendingRestrictionReason,
isChatChannel,
isChatGroup,
isChatSuperGroup,
isUserRightBanned,
} from '../../global/helpers';
import {
selectBot,
selectCanAnimateInterface,
selectChat,
selectChatFullInfo,
selectCurrentMessageList,
selectCurrentMiddleSearch,
selectDraft,
selectIsChatBotNotStarted,
selectIsCurrentUserFrozen,
selectIsInSelectMode,
selectIsMonoforumAdmin,
selectIsRightColumnShown,
selectIsUserBlocked,
selectPeerPaidMessagesStars,
selectPinnedIds,
selectTabState,
selectTheme,
selectThemeValues,
selectThreadInfo,
selectTopic,
selectTopics,
selectUserFullInfo,
} from '../../global/selectors';
import {
IS_ANDROID, IS_ELECTRON, IS_IOS, IS_SAFARI, IS_TRANSLATION_SUPPORTED, MASK_IMAGE_DISABLED,
} from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import buildStyle from '../../util/buildStyle';
import captureEscKeyListener from '../../util/captureEscKeyListener';
import { isUserId } from '../../util/entities/ids';
import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms';
import useAppLayout from '../../hooks/useAppLayout';
import useCustomBackground from '../../hooks/useCustomBackground';
import useForceUpdate from '../../hooks/useForceUpdate';
import useHistoryBack from '../../hooks/useHistoryBack';
import useLang from '../../hooks/useLang';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import { useResize } from '../../hooks/useResize';
import useSyncEffect from '../../hooks/useSyncEffect';
import useWindowSize from '../../hooks/window/useWindowSize';
import usePinnedMessage from './hooks/usePinnedMessage';
import useFluidBackgroundFilter from './message/hooks/useFluidBackgroundFilter';
import Composer from '../common/Composer';
import Icon from '../common/icons/Icon';
import PrivacySettingsNoticeModal from '../common/PrivacySettingsNoticeModal.async';
import SeenByModal from '../common/SeenByModal.async';
import UnpinAllMessagesModal from '../common/UnpinAllMessagesModal.async';
import Button from '../ui/Button';
import Transition from '../ui/Transition';
import ChatLanguageModal from './ChatLanguageModal.async';
import { DropAreaState } from './composer/DropArea';
import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
import FloatingActionButtons from './FloatingActionButtons';
import FrozenAccountPlaceholder from './FrozenAccountPlaceholder';
import MessageList from './MessageList';
import MessageSelectToolbar from './MessageSelectToolbar.async';
import MiddleHeader from './MiddleHeader';
import MiddleHeaderPanes from './MiddleHeaderPanes';
import PremiumRequiredPlaceholder from './PremiumRequiredPlaceholder';
import ReactorListModal from './ReactorListModal.async';
import MiddleSearch from './search/MiddleSearch.async';
import './MiddleColumn.scss';
import styles from './MiddleColumn.module.scss';
interface OwnProps {
leftColumnRef: ElementRef<HTMLDivElement>;
isMobile?: boolean;
}
type StateProps = {
chatId?: string;
threadId?: ThreadId;
isComments?: boolean;
messageListType?: MessageListType;
chat?: ApiChat;
draftReplyInfo?: ApiInputMessageReplyInfo;
isPrivate?: boolean;
isPinnedMessageList?: boolean;
canPost?: boolean;
currentUserBannedRights?: ApiChatBannedRights;
defaultBannedRights?: ApiChatBannedRights;
pinnedMessagesCount?: number;
theme: ThemeKey;
customBackground?: string;
backgroundColor?: string;
patternColor?: string;
isLeftColumnShown?: boolean;
isRightColumnShown?: boolean;
isBackgroundBlurred?: boolean;
leftColumnWidth?: number;
hasActiveMiddleSearch?: boolean;
isSelectModeActive?: boolean;
isSeenByModalOpen: boolean;
isPrivacySettingsNoticeModalOpen: boolean;
isReactorListModalOpen: boolean;
isChatLanguageModalOpen?: boolean;
withInterfaceAnimations?: boolean;
shouldSkipHistoryAnimations?: boolean;
currentTransitionKey: number;
isChannel?: boolean;
arePeerSettingsLoaded?: boolean;
canSubscribe?: boolean;
canStartBot?: boolean;
canRestartBot?: boolean;
shouldLoadFullChat?: boolean;
activeEmojiInteractions?: ActiveEmojiInteraction[];
shouldJoinToSend?: boolean;
shouldSendJoinRequest?: boolean;
pinnedIds?: number[];
canUnpin?: boolean;
canUnblock?: boolean;
isSavedDialog?: boolean;
canShowOpenChatButton?: boolean;
isContactRequirePremium?: boolean;
topics?: Record<number, ApiTopic>;
paidMessagesStars?: number;
isAccountFrozen?: boolean;
freezeAppealChat?: ApiChat;
shouldBlockSendInMonoforum?: boolean;
};
function isImage(item: DataTransferItem) {
return item.kind === 'file' && item.type && SUPPORTED_PHOTO_CONTENT_TYPES.has(item.type);
}
function isVideo(item: DataTransferItem) {
return item.kind === 'file' && item.type && SUPPORTED_VIDEO_CONTENT_TYPES.has(item.type);
}
const LAYER_ANIMATION_DURATION_MS = 450 + ANIMATION_END_DELAY;
function MiddleColumn({
leftColumnRef,
chatId,
threadId,
isComments,
messageListType,
isMobile,
chat,
draftReplyInfo,
isPrivate,
isPinnedMessageList,
canPost,
currentUserBannedRights,
defaultBannedRights,
pinnedMessagesCount,
customBackground,
theme,
backgroundColor,
patternColor,
isLeftColumnShown,
isRightColumnShown,
isBackgroundBlurred,
leftColumnWidth,
hasActiveMiddleSearch,
isSelectModeActive,
isSeenByModalOpen,
isPrivacySettingsNoticeModalOpen,
isReactorListModalOpen,
isChatLanguageModalOpen,
withInterfaceAnimations,
shouldSkipHistoryAnimations,
currentTransitionKey,
isChannel,
arePeerSettingsLoaded,
canSubscribe,
canStartBot,
canRestartBot,
activeEmojiInteractions,
shouldJoinToSend,
shouldSendJoinRequest,
shouldLoadFullChat,
pinnedIds,
canUnpin,
canUnblock,
isSavedDialog,
canShowOpenChatButton,
isContactRequirePremium,
topics,
paidMessagesStars,
isAccountFrozen,
freezeAppealChat,
shouldBlockSendInMonoforum,
}: OwnProps & StateProps) {
const {
openChat,
openPreviousChat,
unpinAllMessages,
loadUser,
loadPeerSettings,
exitMessageSelectMode,
joinChannel,
sendBotCommand,
restartBot,
showNotification,
loadFullChat,
setLeftColumnWidth,
resetLeftColumnWidth,
unblockUser,
} = getActions();
const { width: windowWidth } = useWindowSize();
const { isTablet, isDesktop } = useAppLayout();
const oldLang = useOldLang();
const lang = useLang();
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
const [isScrollDownNeeded, setIsScrollDownShown] = useState(false);
const isScrollDownShown = isScrollDownNeeded && (!isMobile || !hasActiveMiddleSearch);
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
const {
handleIntersectPinnedMessage,
handleFocusPinnedMessage,
getCurrentPinnedIndex,
getLoadingPinnedId,
} = usePinnedMessage(chatId, threadId, pinnedIds);
const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined;
const renderingChatId = usePrevDuringAnimation(chatId, closeAnimationDuration);
const renderingThreadId = usePrevDuringAnimation(threadId, closeAnimationDuration);
const renderingMessageListType = usePrevDuringAnimation(messageListType, closeAnimationDuration);
const renderingCanSubscribe = usePrevDuringAnimation(canSubscribe, closeAnimationDuration);
const renderingCanStartBot = usePrevDuringAnimation(canStartBot, closeAnimationDuration);
const renderingCanRestartBot = usePrevDuringAnimation(canRestartBot, closeAnimationDuration);
const renderingCanUnblock = usePrevDuringAnimation(canUnblock, closeAnimationDuration);
const renderingCanPost = usePrevDuringAnimation(canPost, closeAnimationDuration)
&& !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && !renderingCanUnblock
&& chatId !== TMP_CHAT_ID && !isContactRequirePremium;
const renderingIsScrollDownShown = usePrevDuringAnimation(
isScrollDownShown, closeAnimationDuration,
) && chatId !== TMP_CHAT_ID;
const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration);
const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration);
const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration);
const renderingHandleIntersectPinnedMessage = usePrevDuringAnimation(
chatId ? handleIntersectPinnedMessage : undefined,
closeAnimationDuration,
);
const prevTransitionKey = usePreviousDeprecated(currentTransitionKey);
const cleanupExceptionKey = (
prevTransitionKey !== undefined && prevTransitionKey < currentTransitionKey ? prevTransitionKey : undefined
);
const { isReady, handleCssTransitionEnd, handleSlideTransitionStop } = useIsReady(
!shouldSkipHistoryAnimations && withInterfaceAnimations,
currentTransitionKey,
prevTransitionKey,
chatId,
isMobile,
);
useEffect(() => {
return chatId
? captureEscKeyListener(() => {
openChat({ id: undefined });
})
: undefined;
}, [chatId, openChat]);
useSyncEffect(() => {
setDropAreaState(DropAreaState.None);
setIsNotchShown(undefined);
}, [chatId]);
// Fix for mobile virtual keyboard
useEffect(() => {
if (!IS_IOS && !IS_ANDROID) {
return undefined;
}
const { visualViewport } = window;
if (!visualViewport) {
return undefined;
}
const handleResize = () => {
const isFixNeeded = visualViewport.height !== document.documentElement.clientHeight;
requestMutation(() => {
document.body.classList.toggle('keyboard-visible', isFixNeeded);
requestMeasure(() => {
if (!isFixNeeded && visualViewport.offsetTop) {
requestMutation(() => {
window.scrollTo({ top: 0 });
});
}
});
});
};
visualViewport.addEventListener('resize', handleResize);
return () => {
visualViewport.removeEventListener('resize', handleResize);
};
});
useEffect(() => {
if (isPrivate) {
loadUser({ userId: chatId! });
}
}, [chatId, isPrivate, loadUser]);
useEffect(() => {
if (!arePeerSettingsLoaded) {
loadPeerSettings({ peerId: chatId! });
}
}, [chatId, isPrivate, arePeerSettingsLoaded]);
useEffect(() => {
if (chatId && shouldLoadFullChat && isReady) {
loadFullChat({ chatId });
}
}, [shouldLoadFullChat, chatId, isReady, loadFullChat]);
const {
initResize, resetResize, handleMouseUp,
} = useResize(leftColumnRef, (n) => setLeftColumnWidth({
leftColumnWidth: n,
}), resetLeftColumnWidth, leftColumnWidth, '--left-column-width');
const handleDragEnter = useLastCallback((e: React.DragEvent<HTMLDivElement>) => {
const { items } = e.dataTransfer || {};
// In Safari, the e.dataTransfer.items list may be empty during dragenter/dragover events,
// preventing the ability to determine file types in advance. More details: https://bugs.webkit.org/show_bug.cgi?id=223517
const shouldDrawQuick = IS_SAFARI || (items && items.length > 0 && 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((item) => isImage(item) || isVideo(item)));
setDropAreaState(shouldDrawQuick ? DropAreaState.QuickFile : DropAreaState.Document);
});
const handleHideDropArea = useLastCallback(() => {
setDropAreaState(DropAreaState.None);
});
const handleOpenUnpinModal = useLastCallback(() => {
setIsUnpinModalOpen(true);
});
const closeUnpinModal = useLastCallback(() => {
setIsUnpinModalOpen(false);
});
const handleOpenChatFromSaved = useLastCallback(() => {
openChat({ id: String(threadId) });
});
const handleUnpinAllMessages = useLastCallback(() => {
unpinAllMessages({ chatId: chatId!, threadId: threadId! });
closeUnpinModal();
openPreviousChat();
});
const handleTabletFocus = useLastCallback(() => {
openChat({ id: chatId });
});
const handleSubscribeClick = useLastCallback(() => {
joinChannel({ chatId: chatId! });
if (renderingShouldSendJoinRequest) {
showNotification({
message: isChannel
? oldLang('RequestToJoinChannelSentDescription') : oldLang('RequestToJoinGroupSentDescription'),
});
}
});
const handleStartBot = useLastCallback(() => {
sendBotCommand({ command: '/start' });
});
const handleRestartBot = useLastCallback(() => {
restartBot({ chatId: chatId! });
});
const handleUnblock = useLastCallback(() => {
unblockUser({ userId: chatId! });
});
const customBackgroundValue = useCustomBackground(theme, customBackground);
const className = buildClassName(
MASK_IMAGE_DISABLED ? 'mask-image-disabled' : 'mask-image-enabled',
);
const bgClassName = buildClassName(
styles.background,
styles.withTransition,
customBackground && styles.customBgImage,
backgroundColor && styles.customBgColor,
customBackground && isBackgroundBlurred && styles.blurred,
isRightColumnShown && styles.withRightColumn,
IS_ELECTRON && !(renderingChatId && renderingThreadId) && styles.draggable,
);
const messagingDisabledClassName = buildClassName(
'messaging-disabled',
!isSelectModeActive && 'shown',
);
const messageSendingRestrictionReason = getMessageSendingRestrictionReason(
oldLang, currentUserBannedRights, defaultBannedRights,
);
const forumComposerPlaceholder = getForumComposerPlaceholder(
oldLang, chat, threadId, topics, Boolean(draftReplyInfo),
);
const composerRestrictionMessage = messageSendingRestrictionReason
|| forumComposerPlaceholder
|| (shouldBlockSendInMonoforum ? lang('MonoforumComposerPlaceholder') : undefined)
|| (isContactRequirePremium ? <PremiumRequiredPlaceholder userId={chatId!} /> : undefined)
|| (isAccountFrozen && freezeAppealChat?.id !== chatId ? <FrozenAccountPlaceholder /> : undefined);
// 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',
);
useHistoryBack({
isActive: isSelectModeActive,
onBack: exitMessageSelectMode,
});
// Prepare filter beforehand to avoid flickering
useFluidBackgroundFilter(patternColor);
const isMessagingDisabled = Boolean(
!isPinnedMessageList && !isSavedDialog && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
&& !renderingCanSubscribe && composerRestrictionMessage,
) || (isAccountFrozen && freezeAppealChat?.id !== chatId);
const withMessageListBottomShift = Boolean(
renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot
|| (isPinnedMessageList && canUnpin) || canShowOpenChatButton || renderingCanUnblock,
);
const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive);
return (
<div
id="MiddleColumn"
className={className}
onTransitionEnd={handleCssTransitionEnd}
style={buildStyle(
`--composer-hidden-scale: ${composerHiddenScale}`,
`--toolbar-hidden-scale: ${toolbarHiddenScale}`,
`--unpin-hidden-scale: ${unpinHiddenScale}`,
`--toolbar-unpin-hidden-scale: ${toolbarForUnpinHiddenScale},`,
`--composer-translate-x: ${composerTranslateX}px`,
`--toolbar-translate-x: ${toolbarTranslateX}px`,
`--pattern-color: ${patternColor}`,
backgroundColor && `--theme-background-color: ${backgroundColor}`,
)}
onClick={(isTablet && isLeftColumnShown) ? handleTabletFocus : undefined}
>
{isDesktop && (
<div
className="resize-handle"
onMouseDown={initResize}
onMouseUp={handleMouseUp}
onDoubleClick={resetResize}
/>
)}
<div
className={bgClassName}
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
/>
<div id="middle-column-portals" />
{Boolean(renderingChatId && renderingThreadId) && (
<>
<div className="messages-layout" onDragEnter={renderingCanPost ? handleDragEnter : undefined}>
<MiddleHeaderPanes
key={renderingChatId}
chatId={renderingChatId!}
threadId={renderingThreadId!}
messageListType={renderingMessageListType!}
getCurrentPinnedIndex={getCurrentPinnedIndex}
getLoadingPinnedId={getLoadingPinnedId}
onFocusPinnedMessage={handleFocusPinnedMessage}
/>
<MiddleHeader
chatId={renderingChatId!}
threadId={renderingThreadId!}
messageListType={renderingMessageListType!}
isComments={isComments}
isMobile={isMobile}
getCurrentPinnedIndex={getCurrentPinnedIndex}
getLoadingPinnedId={getLoadingPinnedId}
onFocusPinnedMessage={handleFocusPinnedMessage}
/>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : withInterfaceAnimations ? 'slide' : 'fade'}
activeKey={currentTransitionKey}
shouldCleanup
cleanupExceptionKey={cleanupExceptionKey}
isBlockingAnimation
onStop={handleSlideTransitionStop}
>
<MessageList
key={`${renderingChatId}-${renderingThreadId}-${renderingMessageListType}`}
chatId={renderingChatId!}
threadId={renderingThreadId!}
type={renderingMessageListType!}
isComments={isComments}
canPost={renderingCanPost!}
onScrollDownToggle={setIsScrollDownShown}
onNotchToggle={setIsNotchShown}
isReady={isReady}
isContactRequirePremium={isContactRequirePremium}
paidMessagesStars={paidMessagesStars}
withBottomShift={withMessageListBottomShift}
withDefaultBg={Boolean(!customBackground && !backgroundColor)}
onIntersectPinnedMessage={renderingHandleIntersectPinnedMessage!}
/>
<div className={footerClassName}>
{renderingCanPost && (
<Composer
type="messageList"
chatId={renderingChatId!}
threadId={renderingThreadId!}
messageListType={renderingMessageListType!}
dropAreaState={dropAreaState}
onDropHide={handleHideDropArea}
isReady={isReady}
isMobile={isMobile}
editableInputId={EDITABLE_INPUT_ID}
editableInputCssSelector={EDITABLE_INPUT_CSS_SELECTOR}
inputId="message-input-text"
/>
)}
{isPinnedMessageList && canUnpin && (
<div className="middle-column-footer-button-container" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
color="secondary"
className="composer-button unpin-all-button"
onClick={handleOpenUnpinModal}
>
<Icon name="unpin" />
<span>{oldLang('Chat.Pinned.UnpinAll', pinnedMessagesCount, 'i')}</span>
</Button>
</div>
)}
{canShowOpenChatButton && (
<div className="middle-column-footer-button-container" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
color="secondary"
className="composer-button open-chat-button"
onClick={handleOpenChatFromSaved}
>
<span>{oldLang('SavedOpenChat')}</span>
</Button>
</div>
)}
{isMessagingDisabled && (
<div className={messagingDisabledClassName}>
<div className="messaging-disabled-inner">
<span>
{composerRestrictionMessage}
</span>
</div>
</div>
)}
{(
isMobile && (renderingCanSubscribe || (renderingShouldJoinToSend && !renderingShouldSendJoinRequest))
) && (
<div className="middle-column-footer-button-container" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
ripple
className="composer-button join-subscribe-button"
onClick={handleSubscribeClick}
>
{oldLang(renderingIsChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')}
</Button>
</div>
)}
{isMobile && renderingShouldSendJoinRequest && (
<div className="middle-column-footer-button-container" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
ripple
className="composer-button join-subscribe-button"
onClick={handleSubscribeClick}
>
{oldLang('ChannelJoinRequest')}
</Button>
</div>
)}
{isMobile && renderingCanStartBot && (
<div className="middle-column-footer-button-container" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
ripple
className="composer-button join-subscribe-button"
onClick={handleStartBot}
>
{oldLang('BotStart')}
</Button>
</div>
)}
{isMobile && renderingCanRestartBot && (
<div className="middle-column-footer-button-container" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
ripple
className="composer-button join-subscribe-button"
onClick={handleRestartBot}
>
{oldLang('BotRestart')}
</Button>
</div>
)}
{isMobile && renderingCanUnblock && (
<div className="middle-column-footer-button-container" dir={oldLang.isRtl ? 'rtl' : undefined}>
<Button
size="tiny"
fluid
ripple
className="composer-button join-subscribe-button"
onClick={handleUnblock}
>
{oldLang('Unblock')}
</Button>
</div>
)}
<MessageSelectToolbar
messageListType={renderingMessageListType}
isActive={isSelectModeActive}
canPost={renderingCanPost}
/>
<SeenByModal isOpen={isSeenByModalOpen} />
<PrivacySettingsNoticeModal isOpen={isPrivacySettingsNoticeModalOpen} />
<ReactorListModal isOpen={isReactorListModalOpen} />
{IS_TRANSLATION_SUPPORTED && <ChatLanguageModal isOpen={isChatLanguageModalOpen} />}
</div>
</Transition>
<FloatingActionButtons
withScrollDown={renderingIsScrollDownShown}
canPost={renderingCanPost}
withExtraShift={withExtraShift}
/>
</div>
<MiddleSearch isActive={Boolean(hasActiveMiddleSearch)} />
</>
)}
{chatId && (
<UnpinAllMessagesModal
isOpen={isUnpinModalOpen}
chatId={chatId}
pinnedMessagesCount={pinnedMessagesCount}
onClose={closeUnpinModal}
onUnpin={handleUnpinAllMessages}
/>
)}
<div teactFastList>
{activeEmojiInteractions?.map((activeEmojiInteraction, i) => (
<EmojiInteractionAnimation
teactOrderKey={i}
key={activeEmojiInteraction.id}
activeEmojiInteraction={activeEmojiInteraction}
/>
))}
</div>
</div>
);
}
export default memo(withGlobal<OwnProps>(
(global, { isMobile }): StateProps => {
const theme = selectTheme(global);
const {
isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor, patternColor,
} = selectThemeValues(global, theme) || {};
const {
messageLists, isLeftColumnShown, activeEmojiInteractions,
seenByModal, reactorModal, shouldSkipHistoryAnimations,
chatLanguageModal, privacySettingsNoticeModal,
} = selectTabState(global);
const currentMessageList = selectCurrentMessageList(global);
const { leftColumnWidth } = global;
const state: StateProps = {
theme,
customBackground,
backgroundColor,
patternColor,
isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isBackgroundBlurred,
hasActiveMiddleSearch: Boolean(selectCurrentMiddleSearch(global)),
isSelectModeActive: selectIsInSelectMode(global),
isSeenByModalOpen: Boolean(seenByModal),
isPrivacySettingsNoticeModalOpen: Boolean(privacySettingsNoticeModal),
isReactorListModalOpen: Boolean(reactorModal),
isChatLanguageModalOpen: Boolean(chatLanguageModal),
withInterfaceAnimations: selectCanAnimateInterface(global),
currentTransitionKey: Math.max(0, messageLists.length - 1),
activeEmojiInteractions,
leftColumnWidth,
};
if (!currentMessageList) {
return state;
}
const { chatId, threadId, type: messageListType } = currentMessageList;
const isPrivate = isUserId(chatId);
const chat = selectChat(global, chatId);
const bot = selectBot(global, chatId);
const pinnedIds = selectPinnedIds(global, chatId, threadId);
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
const userFullInfo = chatId ? selectUserFullInfo(global, chatId) : undefined;
const threadInfo = selectThreadInfo(global, chatId, threadId);
const isMessageThread = Boolean(!threadInfo?.isCommentsInfo && threadInfo?.fromChannelId);
const topic = selectTopic(global, chatId, threadId);
const canPost = chat && getCanPostInChat(chat, topic, isMessageThread, chatFullInfo);
const isBotNotStarted = selectIsChatBotNotStarted(global, chatId);
const isPinnedMessageList = messageListType === 'pinned';
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
const isChannel = Boolean(chat && isChatChannel(chat));
const canSubscribe = Boolean(
chat && isMainThread && (isChannel || isChatSuperGroup(chat)) && chat.isNotJoined && !chat.joinRequests
&& !chat.isMonoforum,
);
const shouldJoinToSend = Boolean(chat?.isNotJoined && chat.isJoinToSend);
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
const isUserBlocked = isPrivate ? selectIsUserBlocked(global, chatId) : false;
const canRestartBot = Boolean(bot && isUserBlocked);
const canStartBot = !canRestartBot && isBotNotStarted;
const canUnblock = isUserBlocked && !bot;
const shouldLoadFullChat = Boolean(
chat && isChatGroup(chat) && !chatFullInfo,
);
const draftReplyInfo = selectDraft(global, chatId, threadId)?.replyInfo;
const shouldBlockSendInForum = chat?.isForum
? threadId === MAIN_THREAD_ID && !draftReplyInfo && (selectTopic(global, chatId, GENERAL_TOPIC_ID)?.isClosed)
: false;
const isMonoforumAdmin = selectIsMonoforumAdmin(global, chatId);
const shouldBlockSendInMonoforum = Boolean(chat?.isMonoforum && !draftReplyInfo && isMonoforumAdmin);
const topics = selectTopics(global, chatId);
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
const canUnpin = chat && (
isPrivate || (
chat?.isCreator || (!isChannel && !isUserRightBanned(chat, 'pinMessages'))
|| getHasAdminRight(chat, 'pinMessages')
)
);
const userFull = selectUserFullInfo(global, chatId);
const isContactRequirePremium = userFull?.isContactRequirePremium;
const paidMessagesStars = selectPeerPaidMessagesStars(global, chatId);
const isAccountFrozen = selectIsCurrentUserFrozen(global);
const botFreezeAppealId = global.botFreezeAppealId;
const freezeAppealChat = botFreezeAppealId
? selectChat(global, botFreezeAppealId) : undefined;
return {
...state,
chatId,
threadId,
messageListType,
chat,
draftReplyInfo,
isPrivate,
arePeerSettingsLoaded: Boolean(userFullInfo?.settings),
isComments: isMessageThread,
canPost:
!isPinnedMessageList
&& (!chat || canPost)
&& !isBotNotStarted
&& !(shouldJoinToSend && chat?.isNotJoined)
&& !shouldBlockSendInForum
&& !shouldBlockSendInMonoforum
&& !isSavedDialog
&& (!isAccountFrozen || freezeAppealChat?.id === chatId),
isPinnedMessageList,
currentUserBannedRights: chat?.currentUserBannedRights,
defaultBannedRights: chat?.defaultBannedRights,
pinnedMessagesCount: pinnedIds ? pinnedIds.length : 0,
shouldSkipHistoryAnimations,
isChannel,
canSubscribe,
canStartBot,
canRestartBot,
shouldJoinToSend,
shouldSendJoinRequest,
shouldLoadFullChat,
pinnedIds,
canUnpin,
canUnblock,
isSavedDialog,
canShowOpenChatButton,
isContactRequirePremium,
topics,
paidMessagesStars,
isAccountFrozen,
freezeAppealChat,
shouldBlockSendInMonoforum,
};
},
)(MiddleColumn));
function useIsReady(
withAnimations?: boolean,
currentTransitionKey?: number,
prevTransitionKey?: number,
chatId?: string,
isMobile?: boolean,
) {
const [isReady, setIsReady] = useState(!isMobile);
const forceUpdate = useForceUpdate();
const willSwitchMessageList = prevTransitionKey !== undefined && prevTransitionKey !== currentTransitionKey;
if (willSwitchMessageList) {
if (withAnimations) {
setIsReady(false);
// Make sure to end even if end callback was not called (which was some hardly-reproducible bug)
setTimeout(() => {
setIsReady(true);
}, LAYER_ANIMATION_DURATION_MS);
} else {
forceUpdate();
}
}
useSyncEffect(() => {
if (!withAnimations) {
setIsReady(true);
}
}, [withAnimations]);
function handleCssTransitionEnd(e: React.TransitionEvent<HTMLDivElement>) {
if (e.propertyName === 'transform' && e.target === e.currentTarget) {
setIsReady(Boolean(chatId));
}
}
function handleSlideTransitionStop() {
setIsReady(true);
}
return {
isReady: isReady && !willSwitchMessageList,
handleCssTransitionEnd: withAnimations ? handleCssTransitionEnd : undefined,
handleSlideTransitionStop: withAnimations ? handleSlideTransitionStop : undefined,
};
}