824 lines
28 KiB
TypeScript
824 lines
28 KiB
TypeScript
import React, {
|
|
useEffect, useState, memo, useMemo,
|
|
} from '../../lib/teact/teact';
|
|
import { requestMeasure, requestMutation } from '../../lib/fasterdom/fasterdom';
|
|
import { getActions, withGlobal } from '../../global';
|
|
|
|
import type { ApiChat, ApiChatBannedRights } from '../../api/types';
|
|
import { MAIN_THREAD_ID } from '../../api/types';
|
|
import type {
|
|
MessageListType,
|
|
ActiveEmojiInteraction,
|
|
} from '../../global/types';
|
|
import type { ThemeKey } from '../../types';
|
|
|
|
import {
|
|
MOBILE_SCREEN_MAX_WIDTH,
|
|
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
|
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
|
ANIMATION_END_DELAY,
|
|
SUPPORTED_IMAGE_CONTENT_TYPES,
|
|
GENERAL_TOPIC_ID,
|
|
TMP_CHAT_ID,
|
|
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
|
|
} from '../../config';
|
|
import {
|
|
IS_ANDROID, IS_IOS, IS_TRANSLATION_SUPPORTED, MASK_IMAGE_DISABLED,
|
|
} from '../../util/windowEnvironment';
|
|
import { DropAreaState } from './composer/DropArea';
|
|
import {
|
|
selectCanAnimateInterface,
|
|
selectChat,
|
|
selectBot,
|
|
selectChatFullInfo,
|
|
selectChatMessage,
|
|
selectCurrentMessageList,
|
|
selectCurrentTextSearch,
|
|
selectIsChatBotNotStarted,
|
|
selectIsInSelectMode,
|
|
selectIsRightColumnShown,
|
|
selectIsUserBlocked,
|
|
selectPinnedIds,
|
|
selectReplyingToId,
|
|
selectTabState,
|
|
selectTheme,
|
|
selectThreadInfo,
|
|
selectThreadTopMessageId,
|
|
} from '../../global/selectors';
|
|
import {
|
|
getCanPostInChat,
|
|
getMessageSendingRestrictionReason,
|
|
getForumComposerPlaceholder,
|
|
isChatChannel,
|
|
isChatGroup,
|
|
isChatSuperGroup,
|
|
isUserId,
|
|
isUserRightBanned,
|
|
getHasAdminRight,
|
|
} from '../../global/helpers';
|
|
import calculateMiddleFooterTransforms from './helpers/calculateMiddleFooterTransforms';
|
|
import captureEscKeyListener from '../../util/captureEscKeyListener';
|
|
import buildClassName from '../../util/buildClassName';
|
|
import buildStyle from '../../util/buildStyle';
|
|
|
|
import useLastCallback from '../../hooks/useLastCallback';
|
|
import useCustomBackground from '../../hooks/useCustomBackground';
|
|
import useWindowSize from '../../hooks/useWindowSize';
|
|
import usePrevDuringAnimation from '../../hooks/usePrevDuringAnimation';
|
|
import useLang from '../../hooks/useLang';
|
|
import useHistoryBack from '../../hooks/useHistoryBack';
|
|
import usePrevious from '../../hooks/usePrevious';
|
|
import useForceUpdate from '../../hooks/useForceUpdate';
|
|
import useSyncEffect from '../../hooks/useSyncEffect';
|
|
import useAppLayout from '../../hooks/useAppLayout';
|
|
import usePinnedMessage from './hooks/usePinnedMessage';
|
|
import { useResize } from '../../hooks/useResize';
|
|
|
|
import Transition from '../ui/Transition';
|
|
import MiddleHeader from './MiddleHeader';
|
|
import MessageList from './MessageList';
|
|
import FloatingActionButtons from './FloatingActionButtons';
|
|
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 SeenByModal from '../common/SeenByModal.async';
|
|
import EmojiInteractionAnimation from './EmojiInteractionAnimation.async';
|
|
import ReactorListModal from './ReactorListModal.async';
|
|
import GiftPremiumModal from '../main/premium/GiftPremiumModal.async';
|
|
import MessageLanguageModal from './MessageLanguageModal.async';
|
|
|
|
import './MiddleColumn.scss';
|
|
|
|
import styles from './MiddleColumn.module.scss';
|
|
|
|
interface OwnProps {
|
|
leftColumnRef: React.RefObject<HTMLDivElement>;
|
|
isMobile?: boolean;
|
|
}
|
|
|
|
type StateProps = {
|
|
chatId?: string;
|
|
threadId?: number;
|
|
messageListType?: MessageListType;
|
|
chat?: ApiChat;
|
|
replyingToId?: number;
|
|
isPrivate?: boolean;
|
|
isPinnedMessageList?: boolean;
|
|
canPost?: boolean;
|
|
currentUserBannedRights?: ApiChatBannedRights;
|
|
defaultBannedRights?: ApiChatBannedRights;
|
|
hasPinned?: boolean;
|
|
hasAudioPlayer?: boolean;
|
|
hasButtonInHeader?: boolean;
|
|
pinnedMessagesCount?: number;
|
|
theme: ThemeKey;
|
|
customBackground?: string;
|
|
backgroundColor?: string;
|
|
patternColor?: string;
|
|
isLeftColumnShown?: boolean;
|
|
isRightColumnShown?: boolean;
|
|
isBackgroundBlurred?: boolean;
|
|
leftColumnWidth?: number;
|
|
hasCurrentTextSearch?: boolean;
|
|
isSelectModeActive?: boolean;
|
|
isSeenByModalOpen: boolean;
|
|
isReactorListModalOpen: boolean;
|
|
isGiftPremiumModalOpen?: boolean;
|
|
isMessageLanguageModalOpen?: boolean;
|
|
withInterfaceAnimations?: boolean;
|
|
shouldSkipHistoryAnimations?: boolean;
|
|
currentTransitionKey: number;
|
|
isChannel?: boolean;
|
|
areChatSettingsLoaded?: boolean;
|
|
canSubscribe?: boolean;
|
|
canStartBot?: boolean;
|
|
canRestartBot?: boolean;
|
|
shouldLoadFullChat?: boolean;
|
|
activeEmojiInteractions?: ActiveEmojiInteraction[];
|
|
shouldJoinToSend?: boolean;
|
|
shouldSendJoinRequest?: boolean;
|
|
pinnedIds?: number[];
|
|
topMessageId?: number;
|
|
canUnpin?: boolean;
|
|
};
|
|
|
|
function isImage(item: DataTransferItem) {
|
|
return item.kind === 'file' && item.type && SUPPORTED_IMAGE_CONTENT_TYPES.has(item.type);
|
|
}
|
|
|
|
const LAYER_ANIMATION_DURATION_MS = 450 + ANIMATION_END_DELAY;
|
|
|
|
function MiddleColumn({
|
|
leftColumnRef,
|
|
chatId,
|
|
threadId,
|
|
messageListType,
|
|
isMobile,
|
|
chat,
|
|
replyingToId,
|
|
isPrivate,
|
|
isPinnedMessageList,
|
|
canPost,
|
|
currentUserBannedRights,
|
|
defaultBannedRights,
|
|
hasPinned,
|
|
hasAudioPlayer,
|
|
hasButtonInHeader,
|
|
pinnedMessagesCount,
|
|
customBackground,
|
|
theme,
|
|
backgroundColor,
|
|
patternColor,
|
|
isLeftColumnShown,
|
|
isRightColumnShown,
|
|
isBackgroundBlurred,
|
|
leftColumnWidth,
|
|
hasCurrentTextSearch,
|
|
isSelectModeActive,
|
|
isSeenByModalOpen,
|
|
isReactorListModalOpen,
|
|
isGiftPremiumModalOpen,
|
|
isMessageLanguageModalOpen,
|
|
withInterfaceAnimations,
|
|
shouldSkipHistoryAnimations,
|
|
currentTransitionKey,
|
|
isChannel,
|
|
areChatSettingsLoaded,
|
|
canSubscribe,
|
|
canStartBot,
|
|
canRestartBot,
|
|
activeEmojiInteractions,
|
|
shouldJoinToSend,
|
|
shouldSendJoinRequest,
|
|
shouldLoadFullChat,
|
|
pinnedIds,
|
|
topMessageId,
|
|
canUnpin,
|
|
}: OwnProps & StateProps) {
|
|
const {
|
|
openChat,
|
|
openPreviousChat,
|
|
unpinAllMessages,
|
|
loadUser,
|
|
loadChatSettings,
|
|
closeLocalTextSearch,
|
|
exitMessageSelectMode,
|
|
joinChannel,
|
|
sendBotCommand,
|
|
restartBot,
|
|
showNotification,
|
|
loadFullChat,
|
|
setLeftColumnWidth,
|
|
resetLeftColumnWidth,
|
|
} = getActions();
|
|
|
|
const { width: windowWidth } = useWindowSize();
|
|
const { isTablet, isDesktop } = useAppLayout();
|
|
|
|
const lang = useLang();
|
|
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
|
|
const [isFabShown, setIsFabShown] = useState<boolean | undefined>();
|
|
const [isNotchShown, setIsNotchShown] = useState<boolean | undefined>();
|
|
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
|
|
|
|
const {
|
|
onIntersectionChanged,
|
|
onFocusPinnedMessage,
|
|
getCurrentPinnedIndexes,
|
|
getLoadingPinnedId,
|
|
getForceNextPinnedInHeader,
|
|
} = usePinnedMessage(chatId, threadId, pinnedIds, topMessageId);
|
|
|
|
const isMobileSearchActive = isMobile && hasCurrentTextSearch;
|
|
const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined;
|
|
const hasTools = hasPinned && (
|
|
windowWidth < MOBILE_SCREEN_MAX_WIDTH
|
|
|| hasAudioPlayer
|
|
|| (
|
|
isRightColumnShown && windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
|
&& windowWidth < SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
|
)
|
|
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES)
|
|
);
|
|
|
|
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 renderingCanPost = usePrevDuringAnimation(canPost, closeAnimationDuration)
|
|
&& !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && chatId !== TMP_CHAT_ID;
|
|
const renderingHasTools = usePrevDuringAnimation(hasTools, closeAnimationDuration);
|
|
const renderingIsFabShown = usePrevDuringAnimation(isFabShown, closeAnimationDuration) && chatId !== TMP_CHAT_ID;
|
|
const renderingIsChannel = usePrevDuringAnimation(isChannel, closeAnimationDuration);
|
|
const renderingShouldJoinToSend = usePrevDuringAnimation(shouldJoinToSend, closeAnimationDuration);
|
|
const renderingShouldSendJoinRequest = usePrevDuringAnimation(shouldSendJoinRequest, closeAnimationDuration);
|
|
const renderingOnPinnedIntersectionChange = usePrevDuringAnimation(
|
|
chatId ? onIntersectionChanged : undefined,
|
|
closeAnimationDuration,
|
|
);
|
|
|
|
const prevTransitionKey = usePrevious(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 (!areChatSettingsLoaded) {
|
|
loadChatSettings({ chatId: chatId! });
|
|
}
|
|
}, [chatId, isPrivate, areChatSettingsLoaded]);
|
|
|
|
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 || {};
|
|
const shouldDrawQuick = 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')
|
|
// As of September 2021, native clients suggest "send quick, but compressed" only for images
|
|
.every(isImage);
|
|
|
|
setDropAreaState(shouldDrawQuick ? DropAreaState.QuickFile : DropAreaState.Document);
|
|
});
|
|
|
|
const handleHideDropArea = useLastCallback(() => {
|
|
setDropAreaState(DropAreaState.None);
|
|
});
|
|
|
|
const handleOpenUnpinModal = useLastCallback(() => {
|
|
setIsUnpinModalOpen(true);
|
|
});
|
|
|
|
const closeUnpinModal = useLastCallback(() => {
|
|
setIsUnpinModalOpen(false);
|
|
});
|
|
|
|
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 ? lang('RequestToJoinChannelSentDescription') : lang('RequestToJoinGroupSentDescription'),
|
|
});
|
|
}
|
|
});
|
|
|
|
const handleStartBot = useLastCallback(() => {
|
|
sendBotCommand({ command: '/start' });
|
|
});
|
|
|
|
const handleRestartBot = useLastCallback(() => {
|
|
restartBot({ chatId: chatId! });
|
|
});
|
|
|
|
const customBackgroundValue = useCustomBackground(theme, customBackground);
|
|
|
|
const className = buildClassName(
|
|
renderingHasTools && 'has-header-tools',
|
|
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,
|
|
);
|
|
|
|
const messagingDisabledClassName = buildClassName(
|
|
'messaging-disabled',
|
|
!isSelectModeActive && 'shown',
|
|
);
|
|
|
|
const messageSendingRestrictionReason = getMessageSendingRestrictionReason(
|
|
lang, currentUserBannedRights, defaultBannedRights,
|
|
);
|
|
const forumComposerPlaceholder = getForumComposerPlaceholder(lang, chat, threadId, Boolean(replyingToId));
|
|
|
|
const composerRestrictionMessage = messageSendingRestrictionReason || forumComposerPlaceholder;
|
|
|
|
// 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,
|
|
});
|
|
|
|
useHistoryBack({
|
|
isActive: isMobileSearchActive,
|
|
onBack: closeLocalTextSearch,
|
|
});
|
|
|
|
const isMessagingDisabled = Boolean(
|
|
!isPinnedMessageList && !renderingCanPost && !renderingCanRestartBot && !renderingCanStartBot
|
|
&& !renderingCanSubscribe && composerRestrictionMessage,
|
|
);
|
|
const withMessageListBottomShift = Boolean(
|
|
renderingCanRestartBot || renderingCanSubscribe || renderingShouldSendJoinRequest || renderingCanStartBot
|
|
|| isPinnedMessageList,
|
|
);
|
|
const withExtraShift = Boolean(isMessagingDisabled || isSelectModeActive || isPinnedMessageList);
|
|
|
|
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}>
|
|
<MiddleHeader
|
|
chatId={renderingChatId!}
|
|
threadId={renderingThreadId!}
|
|
messageListType={renderingMessageListType!}
|
|
isReady={isReady}
|
|
isMobile={isMobile}
|
|
getCurrentPinnedIndexes={getCurrentPinnedIndexes}
|
|
getLoadingPinnedId={getLoadingPinnedId}
|
|
onFocusPinnedMessage={onFocusPinnedMessage}
|
|
/>
|
|
<Transition
|
|
name={shouldSkipHistoryAnimations ? 'none' : withInterfaceAnimations ? 'slide' : 'fade'}
|
|
activeKey={currentTransitionKey}
|
|
shouldCleanup
|
|
cleanupExceptionKey={cleanupExceptionKey}
|
|
onStop={handleSlideTransitionStop}
|
|
>
|
|
<MessageList
|
|
key={`${renderingChatId}-${renderingThreadId}-${renderingMessageListType}`}
|
|
chatId={renderingChatId!}
|
|
threadId={renderingThreadId!}
|
|
type={renderingMessageListType!}
|
|
canPost={renderingCanPost!}
|
|
hasTools={renderingHasTools}
|
|
onFabToggle={setIsFabShown}
|
|
onNotchToggle={setIsNotchShown}
|
|
isReady={isReady}
|
|
withBottomShift={withMessageListBottomShift}
|
|
withDefaultBg={Boolean(!customBackground && !backgroundColor)}
|
|
onPinnedIntersectionChange={renderingOnPinnedIntersectionChange!}
|
|
getForceNextPinnedInHeader={getForceNextPinnedInHeader}
|
|
/>
|
|
<div className={footerClassName}>
|
|
{renderingCanPost && (
|
|
<Composer
|
|
chatId={renderingChatId!}
|
|
threadId={renderingThreadId!}
|
|
messageListType={renderingMessageListType!}
|
|
dropAreaState={dropAreaState}
|
|
onDropHide={handleHideDropArea}
|
|
isReady={isReady}
|
|
isMobile={isMobile}
|
|
/>
|
|
)}
|
|
{isPinnedMessageList && canUnpin && (
|
|
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
|
|
<Button
|
|
size="tiny"
|
|
fluid
|
|
color="secondary"
|
|
className="unpin-all-button"
|
|
onClick={handleOpenUnpinModal}
|
|
>
|
|
<i className="icon icon-unpin" />
|
|
<span>{lang('Chat.Pinned.UnpinAll', pinnedMessagesCount, 'i')}</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={lang.isRtl ? 'rtl' : undefined}>
|
|
<Button
|
|
size="tiny"
|
|
fluid
|
|
ripple
|
|
className="join-subscribe-button"
|
|
onClick={handleSubscribeClick}
|
|
>
|
|
{lang(renderingIsChannel ? 'ProfileJoinChannel' : 'ProfileJoinGroup')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{isMobile && renderingShouldSendJoinRequest && (
|
|
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
|
|
<Button
|
|
size="tiny"
|
|
fluid
|
|
ripple
|
|
className="join-subscribe-button"
|
|
onClick={handleSubscribeClick}
|
|
>
|
|
{lang('ChannelJoinRequest')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{isMobile && renderingCanStartBot && (
|
|
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
|
|
<Button
|
|
size="tiny"
|
|
fluid
|
|
ripple
|
|
className="join-subscribe-button"
|
|
onClick={handleStartBot}
|
|
>
|
|
{lang('BotStart')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
{isMobile && renderingCanRestartBot && (
|
|
<div className="middle-column-footer-button-container" dir={lang.isRtl ? 'rtl' : undefined}>
|
|
<Button
|
|
size="tiny"
|
|
fluid
|
|
ripple
|
|
className="join-subscribe-button"
|
|
onClick={handleRestartBot}
|
|
>
|
|
{lang('BotRestart')}
|
|
</Button>
|
|
</div>
|
|
)}
|
|
<MessageSelectToolbar
|
|
messageListType={renderingMessageListType}
|
|
isActive={isSelectModeActive}
|
|
canPost={renderingCanPost}
|
|
/>
|
|
<SeenByModal isOpen={isSeenByModalOpen} />
|
|
<ReactorListModal isOpen={isReactorListModalOpen} />
|
|
{IS_TRANSLATION_SUPPORTED && <MessageLanguageModal isOpen={isMessageLanguageModalOpen} />}
|
|
</div>
|
|
</Transition>
|
|
|
|
<FloatingActionButtons
|
|
isShown={renderingIsFabShown!}
|
|
canPost={renderingCanPost}
|
|
withExtraShift={withExtraShift}
|
|
/>
|
|
</div>
|
|
{isMobile && <MobileSearch isActive={Boolean(isMobileSearchActive)} />}
|
|
</>
|
|
)}
|
|
{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>
|
|
<GiftPremiumModal isOpen={isGiftPremiumModalOpen} />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default memo(withGlobal<OwnProps>(
|
|
(global, { isMobile }): StateProps => {
|
|
const theme = selectTheme(global);
|
|
const {
|
|
isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor, patternColor,
|
|
} = global.settings.themes[theme] || {};
|
|
|
|
const {
|
|
messageLists, isLeftColumnShown, activeEmojiInteractions,
|
|
seenByModal, giftPremiumModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
|
|
messageLanguageModal,
|
|
} = selectTabState(global);
|
|
const currentMessageList = selectCurrentMessageList(global);
|
|
const { leftColumnWidth } = global;
|
|
|
|
const state: StateProps = {
|
|
theme,
|
|
customBackground,
|
|
backgroundColor,
|
|
patternColor,
|
|
isLeftColumnShown,
|
|
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
|
|
isBackgroundBlurred,
|
|
hasCurrentTextSearch: Boolean(selectCurrentTextSearch(global)),
|
|
isSelectModeActive: selectIsInSelectMode(global),
|
|
isSeenByModalOpen: Boolean(seenByModal),
|
|
isReactorListModalOpen: Boolean(reactorModal),
|
|
isGiftPremiumModalOpen: giftPremiumModal?.isOpen,
|
|
isMessageLanguageModalOpen: Boolean(messageLanguageModal),
|
|
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 { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
|
|
|
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
|
const isComments = Boolean(threadInfo?.originChannelId);
|
|
const canPost = chat && getCanPostInChat(chat, threadId, isComments);
|
|
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,
|
|
);
|
|
const shouldJoinToSend = Boolean(chat?.isNotJoined && chat.isJoinToSend);
|
|
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
|
const canRestartBot = Boolean(bot && selectIsUserBlocked(global, bot.id));
|
|
const canStartBot = !canRestartBot && isBotNotStarted;
|
|
const shouldLoadFullChat = Boolean(
|
|
chat && isChatGroup(chat) && !selectChatFullInfo(global, chat.id),
|
|
);
|
|
const replyingToId = selectReplyingToId(global, chatId, threadId);
|
|
const shouldBlockSendInForum = chat?.isForum
|
|
? threadId === MAIN_THREAD_ID && !replyingToId && (chat.topics?.[GENERAL_TOPIC_ID]?.isClosed)
|
|
: false;
|
|
const audioMessage = audioChatId && audioMessageId
|
|
? selectChatMessage(global, audioChatId, audioMessageId)
|
|
: undefined;
|
|
|
|
const isCommentThread = threadId !== MAIN_THREAD_ID && !chat?.isForum;
|
|
const topMessageId = isCommentThread ? selectThreadTopMessageId(global, chatId, threadId) : undefined;
|
|
|
|
const canUnpin = chat && (
|
|
isPrivate || (
|
|
chat?.isCreator || (!isChannel && !isUserRightBanned(chat, 'pinMessages'))
|
|
|| getHasAdminRight(chat, 'pinMessages')
|
|
)
|
|
);
|
|
|
|
return {
|
|
...state,
|
|
chatId,
|
|
threadId,
|
|
messageListType,
|
|
chat,
|
|
replyingToId,
|
|
isPrivate,
|
|
areChatSettingsLoaded: Boolean(chat?.settings),
|
|
canPost: !isPinnedMessageList
|
|
&& (!chat || canPost)
|
|
&& !isBotNotStarted
|
|
&& !(shouldJoinToSend && chat?.isNotJoined)
|
|
&& !shouldBlockSendInForum,
|
|
isPinnedMessageList,
|
|
currentUserBannedRights: chat?.currentUserBannedRights,
|
|
defaultBannedRights: chat?.defaultBannedRights,
|
|
hasPinned: isCommentThread || Boolean(!isPinnedMessageList && pinnedIds?.length),
|
|
hasAudioPlayer: Boolean(audioMessage),
|
|
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
|
|
pinnedMessagesCount: pinnedIds ? pinnedIds.length : 0,
|
|
shouldSkipHistoryAnimations,
|
|
isChannel,
|
|
canSubscribe,
|
|
canStartBot,
|
|
canRestartBot,
|
|
shouldJoinToSend,
|
|
shouldSendJoinRequest,
|
|
shouldLoadFullChat,
|
|
pinnedIds,
|
|
topMessageId,
|
|
canUnpin,
|
|
};
|
|
},
|
|
)(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,
|
|
};
|
|
}
|