TelegramPWA/src/components/middle/MiddleHeader.tsx

430 lines
14 KiB
TypeScript

import type { FC } from '../../lib/teact/teact';
import type React from '../../lib/teact/teact';
import {
memo, useRef,
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type {
ApiChat, ApiMessage, ApiSticker, ApiTypingStatus,
} from '../../api/types';
import type { GlobalState } from '../../global/types';
import type { Signal } from '../../util/signals';
import { MAIN_THREAD_ID } from '../../api/types';
import { type MessageListType, StoryViewerOrigin, type ThreadId } from '../../types';
import {
EDITABLE_INPUT_CSS_SELECTOR,
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
} from '../../config';
import {
getIsSavedDialog,
} from '../../global/helpers';
import {
selectChat,
selectChatMessage,
selectCustomEmoji,
selectIsChatWithSelf,
selectIsInSelectMode,
selectIsRightColumnShown,
selectPeer,
selectPinnedIds,
selectScheduledIds,
selectTabState,
} from '../../global/selectors';
import { selectThreadInfo, selectThreadLocalStateParam } from '../../global/selectors/threads';
import { IS_TAURI } from '../../util/browser/globalEnvironment';
import { IS_MAC_OS } from '../../util/browser/windowEnvironment';
import buildClassName from '../../util/buildClassName';
import { isUserId } from '../../util/entities/ids';
import useAppLayout from '../../hooks/useAppLayout';
import useConnectionStatus from '../../hooks/useConnectionStatus';
import useLastCallback from '../../hooks/useLastCallback';
import useLongPress from '../../hooks/useLongPress';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useWindowSize from '../../hooks/window/useWindowSize';
import GroupChatInfo from '../common/GroupChatInfo';
import PrivateChatInfo from '../common/PrivateChatInfo';
import UnreadCounter from '../common/UnreadCounter';
import Button from '../ui/Button';
import Transition from '../ui/Transition';
import HeaderActions from './HeaderActions';
import AudioPlayer from './panes/AudioPlayer';
import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
import './MiddleHeader.scss';
const BACK_BUTTON_INACTIVE_TIME = 450;
const EMOJI_STATUS_SIZE = 22;
const SEARCH_LONGTAP_THRESHOLD = 500;
type OwnProps = {
chatId: string;
threadId: ThreadId;
messageListType: MessageListType;
isComments?: boolean;
isMobile?: boolean;
getCurrentPinnedIndex: Signal<number>;
getLoadingPinnedId: Signal<number | undefined>;
onFocusPinnedMessage: (messageId: number) => void;
};
type StateProps = {
chat?: ApiChat;
isSavedDialog?: boolean;
typingStatus?: ApiTypingStatus;
isSelectModeActive?: boolean;
isLeftColumnShown?: boolean;
isRightColumnShown?: boolean;
audioMessage?: ApiMessage;
messagesCount?: number;
isChatWithSelf?: boolean;
shouldSkipHistoryAnimations?: boolean;
currentTransitionKey: number;
connectionState?: GlobalState['connectionState'];
isSyncing?: boolean;
isFetchingDifference?: boolean;
emojiStatusSticker?: ApiSticker;
emojiStatusSlug?: string;
};
const MiddleHeader: FC<OwnProps & StateProps> = ({
chatId,
threadId,
messageListType,
isMobile,
typingStatus,
isSelectModeActive,
isLeftColumnShown,
audioMessage,
chat,
messagesCount,
isComments,
isChatWithSelf,
shouldSkipHistoryAnimations,
currentTransitionKey,
connectionState,
isSyncing,
isFetchingDifference,
getCurrentPinnedIndex,
getLoadingPinnedId,
emojiStatusSticker,
emojiStatusSlug,
isSavedDialog,
onFocusPinnedMessage,
}) => {
const {
openThreadWithInfo,
openChat,
openPreviousChat,
toggleLeftColumn,
exitMessageSelectMode,
openPremiumModal,
openStickerSet,
updateMiddleSearch,
openUniqueGiftBySlug,
} = getActions();
const lang = useOldLang();
const isBackButtonActive = useRef(true);
const { isDesktop, isTablet } = useAppLayout();
const { width: windowWidth } = useWindowSize();
const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN;
const shouldShowCloseButton = isTablet && isLeftColumnShown;
const componentRef = useRef<HTMLDivElement>();
const handleOpenSearch = useLastCallback(() => {
updateMiddleSearch({ chatId, threadId, update: {} });
});
const handleOpenChat = useLastCallback((event: React.MouseEvent | React.TouchEvent) => {
if ((event.target as Element).closest('.title > .custom-emoji')) return;
// Force close My Profile if clicked on Saved Messages header
openThreadWithInfo({ chatId, threadId, isOwnProfile: false });
});
const {
onMouseDown: handleLongPressMouseDown,
onMouseUp: handleLongPressMouseUp,
onMouseLeave: handleLongPressMouseLeave,
onTouchStart: handleLongPressTouchStart,
onTouchEnd: handleLongPressTouchEnd,
} = useLongPress({
onStart: handleOpenSearch,
onClick: handleOpenChat,
threshold: SEARCH_LONGTAP_THRESHOLD,
});
const setBackButtonActive = useLastCallback(() => {
setTimeout(() => {
isBackButtonActive.current = true;
}, BACK_BUTTON_INACTIVE_TIME);
});
const handleUserStatusClick = useLastCallback(() => {
if (emojiStatusSlug) {
openUniqueGiftBySlug({ slug: emojiStatusSlug });
return;
}
openPremiumModal({ fromUserId: chatId });
});
const handleChannelStatusClick = useLastCallback(() => {
if (emojiStatusSlug) {
openUniqueGiftBySlug({ slug: emojiStatusSlug });
return;
}
openStickerSet({
stickerSetInfo: emojiStatusSticker!.stickerSetInfo,
});
});
const handleBackClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>) => {
if (!isBackButtonActive.current) return;
// Workaround for missing UI when quickly clicking the Back button
isBackButtonActive.current = false;
if (isMobile) {
const messageInput = document.querySelector<HTMLDivElement>(EDITABLE_INPUT_CSS_SELECTOR);
messageInput?.blur();
}
if (isSelectModeActive) {
exitMessageSelectMode();
setBackButtonActive();
return;
}
if (messageListType === 'thread' && currentTransitionKey === 0) {
if (!isTablet || shouldShowCloseButton) {
e.stopPropagation(); // Stop propagation to prevent chat re-opening on tablets
openChat({ id: undefined }, { forceOnHeavyAnimation: true });
} else {
toggleLeftColumn();
}
setBackButtonActive();
return;
}
openPreviousChat();
setBackButtonActive();
});
const prevTransitionKey = usePreviousDeprecated(currentTransitionKey);
const cleanupExceptionKey = (
prevTransitionKey !== undefined && prevTransitionKey < currentTransitionKey ? prevTransitionKey : undefined
);
const isAudioPlayerActive = Boolean(audioMessage);
const isAudioPlayerRendering = isDesktop && isAudioPlayerActive;
const isPinnedMessagesFullWidth = isAudioPlayerActive || !isDesktop;
const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing || isFetchingDifference, true);
function renderInfo() {
if (messageListType === 'thread') {
if (threadId === MAIN_THREAD_ID || isSavedDialog || chat?.isForum) {
return renderChatInfo();
}
}
return (
<>
{renderBackButton()}
<h3>
{messagesCount !== undefined ? (
messageListType === 'thread' ? (
(messagesCount
? lang(isComments ? 'Comments' : 'Replies', messagesCount, 'i')
: lang(isComments ? 'CommentsTitle' : 'RepliesTitle')))
: messageListType === 'pinned' ? (lang('PinnedMessagesCount', messagesCount, 'i'))
: messageListType === 'scheduled' ? (
isChatWithSelf ? lang('Reminders') : lang('messages', messagesCount, 'i')
) : undefined
) : lang('Loading')}
</h3>
</>
);
}
function renderChatInfo() {
// TODO Implement count
const savedMessagesStatus = isSavedDialog ? lang('SavedMessages') : undefined;
const realChatId = isSavedDialog ? String(threadId) : chatId;
const displayChatId = chat?.isMonoforum ? chat.linkedMonoforumId! : realChatId;
return (
<>
{(isLeftColumnHideable || currentTransitionKey > 0) && renderBackButton(shouldShowCloseButton, !isSavedDialog)}
<div
className="chat-info-wrapper"
onMouseDown={handleLongPressMouseDown}
onMouseUp={handleLongPressMouseUp}
onMouseLeave={handleLongPressMouseLeave}
onTouchStart={handleLongPressTouchStart}
onTouchEnd={handleLongPressTouchEnd}
>
{isUserId(displayChatId) ? (
<PrivateChatInfo
key={displayChatId}
userId={displayChatId}
threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus}
status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)}
withFullInfo={threadId === MAIN_THREAD_ID}
withMediaViewer={threadId === MAIN_THREAD_ID}
withStory={!isChatWithSelf}
withUpdatingStatus
isSavedDialog={isSavedDialog}
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
emojiStatusSize={EMOJI_STATUS_SIZE}
noRtl
onEmojiStatusClick={handleUserStatusClick}
/>
) : (
<GroupChatInfo
key={displayChatId}
chatId={displayChatId}
threadId={!isSavedDialog ? threadId : undefined}
typingStatus={typingStatus}
withMonoforumStatus={chat?.isMonoforum}
status={connectionStatusText || savedMessagesStatus}
withDots={Boolean(connectionStatusText)}
withMediaViewer={threadId === MAIN_THREAD_ID}
withFullInfo={threadId === MAIN_THREAD_ID}
withUpdatingStatus
withStory
isSavedDialog={isSavedDialog}
storyViewerOrigin={StoryViewerOrigin.MiddleHeaderAvatar}
emojiStatusSize={EMOJI_STATUS_SIZE}
onEmojiStatusClick={handleChannelStatusClick}
noRtl
/>
)}
</div>
</>
);
}
function renderBackButton(asClose = false, withUnreadCounter = false) {
return (
<div className="back-button">
<Button
round
size="smaller"
color="translucent"
onClick={handleBackClick}
ariaLabel={lang(asClose ? 'Close' : 'Back')}
>
<div className={buildClassName('animated-close-icon', !asClose && 'state-back')} />
</Button>
{withUnreadCounter && <UnreadCounter />}
</div>
);
}
return (
<div className="MiddleHeader" ref={componentRef} data-tauri-drag-region={IS_TAURI && IS_MAC_OS ? true : undefined}>
<Transition
name={shouldSkipHistoryAnimations ? 'none' : 'slideFade'}
activeKey={currentTransitionKey}
shouldCleanup
cleanupExceptionKey={cleanupExceptionKey}
>
{renderInfo()}
</Transition>
{!isPinnedMessagesFullWidth && (
<HeaderPinnedMessage
key={chatId}
chatId={chatId}
threadId={threadId}
messageListType={messageListType}
onFocusPinnedMessage={onFocusPinnedMessage}
getLoadingPinnedId={getLoadingPinnedId}
getCurrentPinnedIndex={getCurrentPinnedIndex}
/>
)}
<div className="header-tools">
{isAudioPlayerRendering && (
<AudioPlayer />
)}
<HeaderActions
chatId={chatId}
threadId={threadId}
messageListType={messageListType}
isMobile={isMobile}
canExpandActions={!isAudioPlayerRendering}
/>
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, {
chatId, threadId, messageListType, isMobile,
}): Complete<StateProps> => {
const {
isLeftColumnShown, shouldSkipHistoryAnimations, audioPlayer, messageLists,
} = selectTabState(global);
const chat = selectChat(global, chatId);
const peer = selectPeer(global, chatId);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId)
: undefined;
let messagesCount: number | undefined;
if (messageListType === 'pinned') {
const pinnedIds = selectPinnedIds(global, chatId, threadId);
messagesCount = pinnedIds?.length;
} else if (messageListType === 'scheduled') {
const scheduledIds = selectScheduledIds(global, chatId, threadId);
messagesCount = scheduledIds?.length;
} else if (messageListType === 'thread' && threadId !== MAIN_THREAD_ID) {
const threadInfo = selectThreadInfo(global, chatId, threadId);
messagesCount = threadInfo?.messagesCount || 0;
}
const typingStatus = selectThreadLocalStateParam(global, chatId, threadId, 'typingStatus');
const emojiStatus = peer?.emojiStatus;
const emojiStatusSticker = emojiStatus && selectCustomEmoji(global, emojiStatus.documentId);
const emojiStatusSlug = emojiStatus?.type === 'collectible' ? emojiStatus.slug : undefined;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
return {
typingStatus,
isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
isSelectModeActive: selectIsInSelectMode(global),
audioMessage,
chat,
messagesCount,
isChatWithSelf: selectIsChatWithSelf(global, chatId),
shouldSkipHistoryAnimations,
currentTransitionKey: Math.max(0, messageLists.length - 1),
connectionState: global.connectionState,
isSyncing: global.isSyncing,
isFetchingDifference: global.isFetchingDifference,
emojiStatusSticker,
emojiStatusSlug,
isSavedDialog,
};
},
)(MiddleHeader));