TelegramPWA/src/components/middle/MiddleColumn.tsx

354 lines
12 KiB
TypeScript

import React, {
FC, useEffect, useState, memo, useMemo, useCallback,
} from '../../lib/teact/teact';
import { withGlobal } from '../../lib/teact/teactn';
import { MAIN_THREAD_ID } from '../../api/types';
import { GlobalActions, MessageListType } from '../../global/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,
} from '../../config';
import { IS_MOBILE_SCREEN, IS_TOUCH_ENV, MASK_IMAGE_DISABLED } from '../../util/environment';
import { DropAreaState } from './composer/DropArea';
import {
selectChat,
selectCurrentMessageList,
selectCurrentTextSearch,
selectIsChatBotNotStarted,
selectIsInSelectMode,
selectIsRightColumnShown,
selectPinnedIds,
} 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 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;
canPost?: boolean;
messageSendingRestrictionReason?: string;
hasPinnedOrAudioMessage?: boolean;
customBackground?: string;
patternColor?: string;
isCustomBackgroundColor?: boolean;
isRightColumnShown?: boolean;
isBackgroundBlurred?: boolean;
isMobileSearchActive?: boolean;
isSelectModeActive?: boolean;
animationLevel?: number;
};
type DispatchProps = Pick<GlobalActions, 'openChat' | 'unpinAllMessages' | 'loadUser'>;
const CLOSE_ANIMATION_DURATION = IS_MOBILE_SCREEN ? 450 + ANIMATION_END_DELAY : undefined;
function canBeQuicklyUploaded(item: DataTransferItem) {
return item.kind === 'file' && item.type && CONTENT_TYPES_FOR_QUICK_UPLOAD.includes(item.type);
}
const MiddleColumn: FC<StateProps & DispatchProps> = ({
chatId,
threadId,
messageListType,
isPrivate,
isPinnedMessageList,
canPost,
messageSendingRestrictionReason,
hasPinnedOrAudioMessage,
customBackground,
patternColor,
isCustomBackgroundColor,
isRightColumnShown,
isBackgroundBlurred,
isMobileSearchActive,
isSelectModeActive,
animationLevel,
openChat,
unpinAllMessages,
loadUser,
}) => {
const { width: windowWidth } = useWindowSize();
const [dropAreaState, setDropAreaState] = useState(DropAreaState.None);
const [isFabShown, setIsFabShown] = useState(false);
const [isUnpinModalOpen, setIsUnpinModalOpen] = useState(false);
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);
useEffect(() => {
return chatId
? captureEscKeyListener(() => {
openChat({ id: undefined });
})
: undefined;
}, [chatId, openChat]);
useEffect(() => {
setDropAreaState(DropAreaState.None);
}, [chatId]);
useEffect(() => {
if (isPrivate) {
loadUser({ userId: chatId });
}
}, [chatId, isPrivate, loadUser]);
const handleDragEnter = useCallback((e: React.DragEvent<HTMLDivElement>) => {
if (IS_TOUCH_ENV) {
return;
}
const { items } = e.dataTransfer || {};
const shouldDrawQuick = items && Array.from(items).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 customBackgroundValue = useCustomBackground(customBackground);
const className = buildClassName(
renderingHasTools && 'has-header-tools',
customBackground && !isCustomBackgroundColor && 'custom-bg-image',
customBackground && isCustomBackgroundColor && 'custom-bg-color',
customBackground && isBackgroundBlurred && 'blurred',
MASK_IMAGE_DISABLED ? 'mask-image-disabled' : 'mask-image-enabled',
);
const messagingDisabledClassName = buildClassName(
'messaging-disabled',
!isSelectModeActive && 'shown',
);
// 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 lang = useLang();
return (
<div
id="MiddleColumn"
className={className}
// @ts-ignore teact-feature
style={`
--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};
`}
>
<div
id="middle-column-bg"
// @ts-ignore
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
/>
<div id="middle-column-portals" />
{renderingChatId && renderingThreadId && (
<>
<div className="messages-layout" onDragEnter={renderingCanPost ? handleDragEnter : undefined}>
<MiddleHeader
chatId={renderingChatId}
threadId={renderingThreadId}
messageListType={renderingMessageListType}
/>
<Transition
name={animationLevel === ANIMATION_LEVEL_MAX ? 'slide' : 'fade'}
activeKey={renderingMessageListType === 'thread' && renderingThreadId === MAIN_THREAD_ID ? 1 : 2}
shouldCleanup
>
{() => (
<>
<MessageList
key={`${renderingChatId}-${renderingThreadId}-${renderingMessageListType}`}
chatId={renderingChatId}
threadId={renderingThreadId}
type={renderingMessageListType}
canPost={renderingCanPost}
hasTools={renderingHasTools}
onFabToggle={setIsFabShown}
/>
<div className={buildClassName('middle-column-footer', !renderingCanPost && 'no-composer')}>
{renderingCanPost && (
<Composer
chatId={renderingChatId}
threadId={renderingThreadId}
messageListType={renderingMessageListType}
dropAreaState={dropAreaState}
onDropHide={handleHideDropArea}
/>
)}
{isPinnedMessageList && (
<div className="unpin-button-container">
<Button
size="tiny"
fluid
color="secondary"
className="unpin-all-button"
onClick={handleOpenUnpinModal}
>
<i className="icon-unpin" />
<span>{lang('Chat.PanelHidePinnedMessages')}</span>
</Button>
</div>
)}
{!isPinnedMessageList && !renderingCanPost && messageSendingRestrictionReason && (
<div className={messagingDisabledClassName}>
<div className="messaging-disabled-inner">
<span>
{messageSendingRestrictionReason}
</span>
</div>
</div>
)}
<MessageSelectToolbar
messageListType={renderingMessageListType}
isActive={isSelectModeActive}
canPost={renderingCanPost}
/>
</div>
</>
)}
</Transition>
<ScrollDownButton isShown={isFabShown} />
</div>
{IS_MOBILE_SCREEN && <MobileSearch isActive={Boolean(isMobileSearchActive)} />}
</>
)}
{chatId && (
<UnpinAllMessagesModal
isOpen={isUnpinModalOpen}
chatId={chatId}
onClose={closeUnpinModal}
onUnpin={handleUnpinAllMessages}
/>
)}
</div>
);
};
export default memo(withGlobal(
(global): StateProps => {
const { isBackgroundBlurred, customBackground, patternColor } = global.settings.byKey;
const isCustomBackgroundColor = Boolean((customBackground || '').match(/^#[a-f\d]{6,8}$/i));
const currentMessageList = selectCurrentMessageList(global);
const { chats: { listIds } } = global;
const state: StateProps = {
customBackground,
patternColor,
isCustomBackgroundColor,
isRightColumnShown: selectIsRightColumnShown(global),
isBackgroundBlurred,
isMobileSearchActive: Boolean(IS_MOBILE_SCREEN && selectCurrentTextSearch(global)),
isSelectModeActive: selectIsInSelectMode(global),
animationLevel: global.settings.byKey.animationLevel,
};
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';
return {
...state,
chatId,
threadId,
messageListType,
isPrivate: isChatPrivate(chatId),
canPost: !isPinnedMessageList && (!chat || canPost) && (!isBotNotStarted || IS_MOBILE_SCREEN),
isPinnedMessageList,
messageSendingRestrictionReason: chat && getMessageSendingRestrictionReason(chat),
hasPinnedOrAudioMessage: (
threadId !== MAIN_THREAD_ID
|| Boolean(pinnedIds && pinnedIds.length)
|| Boolean(audioChatId && audioMessageId)
),
};
},
(setGlobal, actions): DispatchProps => pick(actions, [
'openChat', 'unpinAllMessages', 'loadUser',
]),
)(MiddleColumn));