Middle Header: Refactor subheader tools (#5182)

This commit is contained in:
zubiden 2024-11-27 20:34:05 +04:00 committed by Alexander Zinchuk
parent 896c8b99a8
commit 5a647a0a83
40 changed files with 1047 additions and 896 deletions

View File

@ -280,7 +280,7 @@ export function hideAllChatJoinRequests({
});
}
export function hideChatReportPanel(chat: ApiChat) {
export function hideChatReportPane(chat: ApiChat) {
const { id, accessHash } = chat;
return invokeRequest(new GramJs.messages.HidePeerSettingsBar({

View File

@ -655,7 +655,11 @@
"Statistics" = "Statistics";
"EventLogFilterPinnedMessages" = "Pinned messages";
"UnpinMessageAlertTitle" = "Unpin message";
"PinnedMessage" = "Pinned message";
"PinnedMessageTitleSingle" = "Pinned message";
"PinnedMessageTitle_one" = "Pinned message #{index}";
"PinnedMessageTitle_other" = "Pinned message #{index}";
"AccPinnedMessages" = "Pinned Messages";
"AccUnpinMessage" = "Unpin Message";
"Comments_one" = "{count} Comment";
"Comments_other" = "{count} Comments";
"LeaveAComment" = "Leave a comment";

View File

@ -1,37 +1,14 @@
@use "../../../styles/mixins";
.GroupCallTopPane {
position: absolute;
top: 100%;
left: 0;
right: 0;
height: 2.875rem;
overflow: hidden;
box-shadow: 0 2px 2px var(--color-light-shadow);
@include mixins.header-pane;
display: flex;
flex-direction: row;
justify-content: space-between;
align-items: center;
padding: 0.375rem 0.5rem 0.375rem 0.75rem;
background: var(--color-background);
z-index: -1;
cursor: var(--custom-cursor, pointer);
&::before {
content: "";
display: block;
position: absolute;
top: -0.1875rem;
left: 0;
right: 0;
height: 0.125rem;
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
}
@media (max-width: 600px) {
&.has-pinned-offset {
top: calc(100% + 2.875rem);
}
}
.info {
display: flex;
flex-direction: column;
@ -56,9 +33,3 @@
width: auto;
}
}
@media (min-width: 1440px) {
#Main.right-column-open .MiddleHeader .GroupCallTopPane {
width: calc(100% - var(--right-column-width));
}
}

View File

@ -12,7 +12,7 @@ import buildClassName from '../../../util/buildClassName';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useOldLang from '../../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated';
import useHeaderPane, { type PaneState } from '../../middle/hooks/useHeaderPane';
import AvatarList from '../../common/AvatarList';
import Button from '../../ui/Button';
@ -21,8 +21,8 @@ import './GroupCallTopPane.scss';
type OwnProps = {
chatId: string;
hasPinnedOffset: boolean;
className?: string;
onPaneStateChange?: (state: PaneState) => void;
};
type StateProps = {
@ -37,7 +37,7 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
isActive,
className,
groupCall,
hasPinnedOffset,
onPaneStateChange,
}) => {
const {
requestMasterAndJoinGroupCall,
@ -86,23 +86,24 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
};
}, [groupCall?.id, groupCall?.isLoaded, isActive, subscribeToGroupCallUpdates]);
const {
shouldRender,
transitionClassNames,
} = useShowTransitionDeprecated(Boolean(groupCall && isActive));
const renderingParticipantCount = useCurrentOrPrev(groupCall?.participantsCount, true);
const renderingFetchedParticipants = useCurrentOrPrev(fetchedParticipants, true);
const isRendering = Boolean(groupCall && isActive);
const { ref, shouldRender } = useHeaderPane({
isOpen: isRendering,
onStateChange: onPaneStateChange,
});
if (!shouldRender) return undefined;
return (
<div
ref={ref}
className={buildClassName(
'GroupCallTopPane',
hasPinnedOffset && 'has-pinned-offset',
className,
transitionClassNames,
)}
onClick={handleJoinGroupCall}
>

View File

@ -37,6 +37,7 @@ type StateProps = {
const PinMessageModal: FC<OwnProps & StateProps> = ({
isOpen,
chatId,
messageId,
isChannel,
isGroup,
@ -49,17 +50,17 @@ const PinMessageModal: FC<OwnProps & StateProps> = ({
const handlePinMessageForAll = useCallback(() => {
pinMessage({
messageId, isUnpin: false,
chatId, messageId, isUnpin: false,
});
onClose();
}, [pinMessage, messageId, onClose]);
}, [chatId, messageId, onClose]);
const handlePinMessage = useCallback(() => {
pinMessage({
messageId, isUnpin: false, isOneSide: true, isSilent: true,
chatId, messageId, isUnpin: false, isOneSide: true, isSilent: true,
});
onClose();
}, [messageId, onClose, pinMessage]);
}, [chatId, messageId, onClose]);
const lang = useOldLang();

View File

@ -43,10 +43,6 @@
}
}
.group-call {
position: static !important;
}
.notch {
width: 100%;
height: 0;

View File

@ -260,7 +260,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
)}
</div>
{chat && <GroupCallTopPane chatId={chat.id} hasPinnedOffset={false} className={styles.groupCall} />}
{chat && <GroupCallTopPane chatId={chat.id} />}
<div className={styles.notch} />

View File

@ -8,7 +8,7 @@ import React, {
import { addExtraClass } from '../../lib/teact/teact-dom';
import { getActions, getGlobal, withGlobal } from '../../global';
import type { ApiChatFolder, ApiMessage, ApiUser } from '../../api/types';
import type { ApiChatFolder, ApiUser } from '../../api/types';
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
import { ElectronEvent } from '../../types/electron';
@ -61,10 +61,10 @@ import StickerSetModal from '../common/StickerSetModal.async';
import UnreadCount from '../common/UnreadCounter';
import LeftColumn from '../left/LeftColumn';
import MediaViewer from '../mediaViewer/MediaViewer.async';
import AudioPlayer from '../middle/AudioPlayer';
import ReactionPicker from '../middle/message/reactions/ReactionPicker.async';
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
import MiddleColumn from '../middle/MiddleColumn';
import AudioPlayer from '../middle/panes/AudioPlayer';
import ModalContainer from '../modals/ModalContainer';
import PaymentModal from '../payment/PaymentModal.async';
import ReceiptModal from '../payment/ReceiptModal.async';
@ -107,7 +107,6 @@ type StateProps = {
isForwardModalOpen: boolean;
hasNotifications: boolean;
hasDialogs: boolean;
audioMessage?: ApiMessage;
safeLinkModalUrl?: string;
isHistoryCalendarOpen: boolean;
shouldSkipHistoryAnimations?: boolean;
@ -159,7 +158,6 @@ const Main = ({
isForwardModalOpen,
hasNotifications,
hasDialogs,
audioMessage,
activeGroupCallId,
safeLinkModalUrl,
isHistoryCalendarOpen,
@ -545,7 +543,7 @@ const Main = ({
<DraftRecipientPicker requestedDraft={requestedDraft} />
<Notifications isOpen={hasNotifications} />
<Dialogs isOpen={hasDialogs} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
<AudioPlayer noUi />
<ModalContainer />
<SafeLinkModal url={safeLinkModalUrl} />
<HistoryCalendar isOpen={isHistoryCalendarOpen} />
@ -613,7 +611,6 @@ export default memo(withGlobal<OwnProps>(
openedCustomEmojiSetIds,
shouldSkipHistoryAnimations,
openedGame,
audioPlayer,
isLeftColumnShown,
historyCalendarSelectedAt,
notifications,
@ -630,10 +627,6 @@ export default memo(withGlobal<OwnProps>(
deleteFolderDialogModal,
} = selectTabState(global);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId)
: undefined;
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title;
const { chatId } = selectCurrentMessageList(global) || {};
@ -653,7 +646,6 @@ export default memo(withGlobal<OwnProps>(
isReactionPickerOpen: selectIsReactionPickerOpen(global),
hasNotifications: Boolean(notifications.length),
hasDialogs: Boolean(dialogs.length),
audioMessage,
safeLinkModalUrl,
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
shouldSkipHistoryAnimations,

View File

@ -1,36 +0,0 @@
.ChatReportPanel {
position: absolute;
left: 0;
right: 0;
top: 100%;
display: flex;
align-items: center;
margin-left: auto;
background: var(--color-background);
padding: 0.375rem 0.8125rem 0.25rem 1rem;
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow), inset 0 0.125rem 0.125rem var(--color-light-shadow);
transform: translate3d(0, 0, 0);
transition: opacity 0.15s ease, transform var(--layer-transition);
body.no-page-transitions & {
.ripple-container {
display: none;
}
}
@media (min-width: 1276px) {
transform: translate3d(0, 0, 0);
transition: opacity 0.15s ease, transform var(--layer-transition);
#Main.right-column-open & {
padding-right: calc(var(--right-column-width) + 1rem);
}
}
.UserReportPanel--Button {
margin-left: 0.25rem;
flex: 1 1 50%;
white-space: nowrap;
}
}

View File

@ -1,231 +0,0 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo } from '../../lib/teact/teact';
import { getActions } from '../../global';
import type { ApiMessage } from '../../api/types';
import type { Signal } from '../../util/signals';
import {
getMessageIsSpoiler, getMessageMediaHash, getMessageSingleInlineButton, getMessageVideo,
} from '../../global/helpers';
import buildClassName from '../../util/buildClassName';
import { IS_TOUCH_ENV } from '../../util/windowEnvironment';
import { getPictogramDimensions, REM } from '../common/helpers/mediaDimensions';
import renderText from '../common/helpers/renderText';
import renderKeyboardButtonText from './composer/helpers/renderKeyboardButtonText';
import useDerivedState from '../../hooks/useDerivedState';
import { useFastClick } from '../../hooks/useFastClick';
import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback';
import useMedia from '../../hooks/useMedia';
import useOldLang from '../../hooks/useOldLang';
import useThumbnail from '../../hooks/useThumbnail';
import useAsyncRendering from '../right/hooks/useAsyncRendering';
import AnimatedCounter from '../common/AnimatedCounter';
import MediaSpoiler from '../common/MediaSpoiler';
import MessageSummary from '../common/MessageSummary';
import Button from '../ui/Button';
import ConfirmDialog from '../ui/ConfirmDialog';
import RippleEffect from '../ui/RippleEffect';
import Spinner from '../ui/Spinner';
import Transition from '../ui/Transition';
import PinnedMessageNavigation from './PinnedMessageNavigation';
import styles from './HeaderPinnedMessage.module.scss';
const SHOW_LOADER_DELAY = 450;
const EMOJI_SIZE = 1.125 * REM;
type OwnProps = {
message: ApiMessage;
index: number;
count: number;
customTitle?: string;
className?: string;
onUnpinMessage?: (id: number) => void;
onClick?: (e: React.MouseEvent<HTMLDivElement>) => void;
onAllPinnedClick?: () => void;
getLoadingPinnedId: Signal<number | undefined>;
isFullWidth?: boolean;
};
const HeaderPinnedMessage: FC<OwnProps> = ({
message, count, index, customTitle, className, onUnpinMessage, onClick, onAllPinnedClick,
getLoadingPinnedId, isFullWidth,
}) => {
const { clickBotInlineButton } = getActions();
const lang = useOldLang();
const video = getMessageVideo(message);
const gif = video?.isGif ? video : undefined;
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length);
const mediaThumbnail = useThumbnail(message);
const mediaBlobUrl = useMedia(getMessageMediaHash(message, isVideoThumbnail ? 'full' : 'pictogram'));
const isSpoiler = getMessageIsSpoiler(message);
const isLoading = Boolean(useDerivedState(getLoadingPinnedId));
const canRenderLoader = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
const shouldShowLoader = canRenderLoader && isLoading;
const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag();
const handleUnpinMessage = useLastCallback(() => {
closeUnpinDialog();
if (onUnpinMessage) {
onUnpinMessage(message.id);
}
});
const inlineButton = getMessageSingleInlineButton(message);
const handleInlineButtonClick = useLastCallback(() => {
if (inlineButton) {
clickBotInlineButton({ chatId: message.chatId, messageId: message.id, button: inlineButton });
}
});
const [noHoverColor, markNoHoverColor, unmarkNoHoverColor] = useFlag();
const { handleClick, handleMouseDown } = useFastClick(onClick);
function renderPictogram(thumbDataUri?: string, blobUrl?: string, isFullVideo?: boolean, asSpoiler?: boolean) {
const { width, height } = getPictogramDimensions();
const srcUrl = blobUrl || thumbDataUri;
const shouldRenderVideo = isFullVideo && blobUrl;
return (
<div className={styles.pinnedThumb}>
{thumbDataUri && !asSpoiler && !shouldRenderVideo && (
<img
className={styles.pinnedThumbImage}
src={srcUrl}
width={width}
height={height}
alt=""
draggable={false}
/>
)}
{shouldRenderVideo && !asSpoiler && (
<video
src={blobUrl}
width={width}
height={height}
playsInline
disablePictureInPicture
className={styles.pinnedThumbImage}
/>
)}
{thumbDataUri
&& <MediaSpoiler thumbDataUri={srcUrl} isVisible={Boolean(asSpoiler)} width={width} height={height} />}
</div>
);
}
return (
<div className={buildClassName(
'HeaderPinnedMessageWrapper', styles.root, isFullWidth && 'full-width', className,
)}
>
{(count > 1 || shouldShowLoader) && (
<Button
round
size="smaller"
color="translucent"
ariaLabel={lang('EventLogFilterPinnedMessages')}
onClick={!shouldShowLoader ? onAllPinnedClick : undefined}
>
{isLoading && (
<Spinner
color="blue"
className={buildClassName(
styles.loading, styles.pinListIcon, !shouldShowLoader && styles.pinListIconHidden,
)}
/>
)}
<i
className={buildClassName(
'icon', 'icon-pin-list', styles.pinListIcon, shouldShowLoader && styles.pinListIconHidden,
)}
/>
</Button>
)}
{onUnpinMessage && (
<Button
round
size="smaller"
color="translucent"
ariaLabel={lang('UnpinMessageAlertTitle')}
onClick={openUnpinDialog}
>
<i className="icon icon-close" />
</Button>
)}
<ConfirmDialog
isOpen={isUnpinDialogOpen}
onClose={closeUnpinDialog}
text="Would you like to unpin this message?"
confirmLabel="Unpin"
confirmHandler={handleUnpinMessage}
/>
<div
className={buildClassName(styles.pinnedMessage, noHoverColor && styles.noHover)}
onClick={handleClick}
onMouseDown={handleMouseDown}
dir={lang.isRtl ? 'rtl' : undefined}
>
<PinnedMessageNavigation
count={count}
index={index}
/>
<Transition activeKey={message.id} name="slideVertical" className={styles.pictogramTransition}>
{renderPictogram(
mediaThumbnail,
mediaBlobUrl,
isVideoThumbnail,
isSpoiler,
)}
</Transition>
<div
className={buildClassName(styles.messageText, mediaThumbnail && styles.withMedia)}
dir={lang.isRtl ? 'rtl' : undefined}
>
<div className={styles.title} dir={lang.isRtl ? 'rtl' : undefined}>
{!customTitle && (
<AnimatedCounter text={`${lang('PinnedMessage')} ${index > 0 ? `#${count - index}` : ''}`} />
)}
{customTitle && renderText(customTitle)}
</div>
<Transition activeKey={message.id} name="slideVerticalFade" className={styles.messageTextTransition}>
<p dir="auto" className={styles.summary}>
<MessageSummary
message={message}
noEmoji={Boolean(mediaThumbnail)}
emojiSize={EMOJI_SIZE}
/>
</p>
</Transition>
</div>
<RippleEffect />
{inlineButton && (
<Button
size="tiny"
className={styles.inlineButton}
onClick={handleInlineButtonClick}
shouldStopPropagation
onMouseEnter={!IS_TOUCH_ENV ? markNoHoverColor : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkNoHoverColor : undefined}
>
{renderKeyboardButtonText(lang, inlineButton)}
</Button>
)}
</div>
</div>
);
};
export default memo(HeaderPinnedMessage);

View File

@ -66,6 +66,10 @@
}
}
.first-message-date-group {
padding-top: var(--middle-header-panes-height);
}
&.no-composer {
margin-bottom: 0;
@ -379,17 +383,13 @@
&.scrolled:not(.is-animating) .sticky-date {
position: sticky;
top: 0.625rem;
top: calc(0.625rem + var(--middle-header-panes-height));
}
&.is-animating .message-select-control {
display: none !important;
}
.has-header-tools & .sticky-date {
top: 3.75rem !important;
}
.local-action-message,
.ActionMessage {
margin-top: 0.5rem;

View File

@ -89,7 +89,6 @@ type OwnProps = {
isReady: boolean;
onScrollDownToggle: BooleanToVoidFunction;
onNotchToggle: BooleanToVoidFunction;
hasTools?: boolean;
withBottomShift?: boolean;
withDefaultBg: boolean;
onIntersectPinnedMessage: OnIntersectPinnedMessage;
@ -133,7 +132,6 @@ const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000;
const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000;
const BOTTOM_THRESHOLD = 50;
const UNREAD_DIVIDER_TOP = 10;
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
const SCROLL_DEBOUNCE = 200;
const MESSAGE_ANIMATION_DURATION = 500;
const BOTTOM_FOCUS_MARGIN = 20;
@ -146,7 +144,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
chatId,
threadId,
type,
hasTools,
isChatLoaded,
isForum,
isChannelChat,
@ -405,7 +402,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
}
if (!memoFocusingIdRef.current) {
updateStickyDates(container, hasTools);
updateStickyDates(container);
}
runDebouncedForScroll(() => {
@ -474,7 +471,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
useSyncEffect(
() => forceMeasure(() => rememberScrollPositionRef.current()),
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
[messageIds, isViewportNewest, hasTools, rememberScrollPositionRef],
[messageIds, isViewportNewest, rememberScrollPositionRef],
);
useEffect(
() => rememberScrollPositionRef.current(),
@ -591,7 +588,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0));
} else if (unreadDivider) {
newScrollTop = Math.min(
unreadDivider.offsetTop - (hasTools ? UNREAD_DIVIDER_TOP_WITH_TOOLS : UNREAD_DIVIDER_TOP),
unreadDivider.offsetTop - UNREAD_DIVIDER_TOP,
scrollHeight - scrollOffset,
);
} else {
@ -619,7 +616,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
};
});
// This should match deps for `useSyncEffect` above
}, [messageIds, isViewportNewest, hasTools, getContainerHeight, prevContainerHeightRef, noMessageSendingAnimation]);
}, [messageIds, isViewportNewest, getContainerHeight, prevContainerHeightRef, noMessageSendingAnimation]);
useEffectWithPrevDeps(([prevIsSelectModeActive]) => {
if (prevIsSelectModeActive !== undefined) {

View File

@ -265,7 +265,7 @@ const MessageListContent: FC<OwnProps> = ({
return (
<div
className="message-date-group"
className={buildClassName('message-date-group', dateGroupIndex === 0 && 'first-message-date-group')}
key={dateGroup.datetime}
onMouseDown={preventMessageInputBlur}
teactFastList

View File

@ -20,10 +20,6 @@ import {
EDITABLE_INPUT_CSS_SELECTOR,
EDITABLE_INPUT_ID,
GENERAL_TOPIC_ID,
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
MOBILE_SCREEN_MAX_WIDTH,
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
SUPPORTED_PHOTO_CONTENT_TYPES,
TMP_CHAT_ID,
} from '../../config';
@ -45,7 +41,6 @@ import {
selectCanAnimateInterface,
selectChat,
selectChatFullInfo,
selectChatMessage,
selectCurrentMessageList,
selectCurrentMiddleSearch,
selectDraft,
@ -95,6 +90,7 @@ import FloatingActionButtons from './FloatingActionButtons';
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';
@ -119,9 +115,6 @@ type StateProps = {
canPost?: boolean;
currentUserBannedRights?: ApiChatBannedRights;
defaultBannedRights?: ApiChatBannedRights;
hasPinned?: boolean;
hasAudioPlayer?: boolean;
hasButtonInHeader?: boolean;
pinnedMessagesCount?: number;
theme: ThemeKey;
customBackground?: string;
@ -178,9 +171,6 @@ function MiddleColumn({
canPost,
currentUserBannedRights,
defaultBannedRights,
hasPinned,
hasAudioPlayer,
hasButtonInHeader,
pinnedMessagesCount,
customBackground,
theme,
@ -251,15 +241,6 @@ function MiddleColumn({
} = usePinnedMessage(chatId, threadId, pinnedIds);
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);
@ -271,7 +252,6 @@ function MiddleColumn({
const renderingCanPost = usePrevDuringAnimation(canPost, closeAnimationDuration)
&& !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && !renderingCanUnblock
&& chatId !== TMP_CHAT_ID && !isContactRequirePremium;
const renderingHasTools = usePrevDuringAnimation(hasTools, closeAnimationDuration);
const renderingIsScrollDownShown = usePrevDuringAnimation(
isScrollDownShown, closeAnimationDuration,
) && chatId !== TMP_CHAT_ID;
@ -430,7 +410,6 @@ function MiddleColumn({
const customBackgroundValue = useCustomBackground(theme, customBackground);
const className = buildClassName(
renderingHasTools && 'has-header-tools',
MASK_IMAGE_DISABLED ? 'mask-image-disabled' : 'mask-image-enabled',
);
@ -522,12 +501,20 @@ function MiddleColumn({
{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}
isReady={isReady}
isMobile={isMobile}
getCurrentPinnedIndex={getCurrentPinnedIndex}
getLoadingPinnedId={getLoadingPinnedId}
@ -548,7 +535,6 @@ function MiddleColumn({
type={renderingMessageListType!}
isComments={isComments}
canPost={renderingCanPost!}
hasTools={renderingHasTools}
onScrollDownToggle={setIsScrollDownShown}
onNotchToggle={setIsNotchShown}
isReady={isReady}
@ -728,7 +714,7 @@ export default memo(withGlobal<OwnProps>(
const {
messageLists, isLeftColumnShown, activeEmojiInteractions,
seenByModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
seenByModal, reactorModal, shouldSkipHistoryAnimations,
chatLanguageModal, privacySettingsNoticeModal,
} = selectTabState(global);
const currentMessageList = selectCurrentMessageList(global);
@ -763,7 +749,6 @@ export default memo(withGlobal<OwnProps>(
const chat = selectChat(global, chatId);
const bot = selectBot(global, chatId);
const pinnedIds = selectPinnedIds(global, chatId, threadId);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
const threadInfo = selectThreadInfo(global, chatId, threadId);
@ -790,16 +775,11 @@ export default memo(withGlobal<OwnProps>(
const shouldBlockSendInForum = chat?.isForum
? threadId === MAIN_THREAD_ID && !draftReplyInfo && (selectTopic(global, chatId, GENERAL_TOPIC_ID)?.isClosed)
: false;
const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId)
: undefined;
const topics = selectTopics(global, chatId);
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
const isCommentThread = threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum;
const canUnpin = chat && (
isPrivate || (
chat?.isCreator || (!isChannel && !isUserRightBanned(chat, 'pinMessages'))
@ -829,9 +809,6 @@ export default memo(withGlobal<OwnProps>(
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,

View File

@ -1,33 +1,9 @@
@use "../../styles/mixins";
@mixin mobile-header-styles() {
.AudioPlayer {
@include mixins.header-mobile;
flex-direction: row;
margin-top: 0;
padding: 0.25rem 0.5rem;
&-content {
padding: 0 0.5rem;
flex-grow: 1;
}
> .Button, > .playback-rate-menu {
margin: -0.0625rem 0 0;
}
> .player-close {
margin-left: auto;
}
}
}
.MiddleHeader {
display: flex;
align-items: center;
width: 100%;
box-shadow: 0 2px 2px var(--color-light-shadow);
background: var(--color-background);
position: relative;
z-index: var(--z-middle-header);
@ -118,18 +94,7 @@
}
}
@media (min-width: 1276px) and (max-width: 1439px) {
.HeaderActions {
transform: translate3d(0, 0, 0);
transition: transform var(--layer-transition);
#Main.right-column-open & {
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
}
}
}
@media (min-width: 1440px) {
@media (min-width: 1276px) {
transform: translate3d(0, 0, 0);
transition: transform var(--layer-transition);
@ -144,34 +109,6 @@
}
}
@media (min-width: 1276px) and (max-width: 1439px) {
&:not(.tools-stacked) .AudioPlayer {
opacity: 1;
#Main.right-column-open & {
opacity: 0;
}
}
}
&.tools-stacked .AudioPlayer {
@include mobile-header-styles();
@media (min-width: 1150px) {
#Main.right-column-open & {
padding-right: calc(0.5rem + var(--right-column-width));
}
}
}
&.tools-stacked.animated .AudioPlayer {
animation: fade-in var(--layer-transition) forwards;
body.no-page-transitions & {
animation: none;
}
}
h3 {
font-weight: 500;
font-size: 1.125rem;
@ -384,10 +321,6 @@
}
}
@media (max-width: 600px) {
@include mobile-header-styles();
}
body.is-electron.is-macos & {
-webkit-app-region: drag;
}

View File

@ -1,11 +1,11 @@
import type { FC } from '../../lib/teact/teact';
import React, {
memo, useEffect, useLayoutEffect, useRef,
memo, useRef,
} from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type {
ApiChat, ApiMessage, ApiPeer, ApiSticker, ApiTypingStatus,
ApiChat, ApiMessage, ApiSticker, ApiTypingStatus,
} from '../../api/types';
import type { GlobalState, MessageListType } from '../../global/types';
import type { Signal } from '../../util/signals';
@ -14,35 +14,18 @@ import { StoryViewerOrigin, type ThreadId } from '../../types';
import {
EDITABLE_INPUT_CSS_SELECTOR,
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
MOBILE_SCREEN_MAX_WIDTH,
SAFE_SCREEN_WIDTH_FOR_CHAT_INFO,
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
} from '../../config';
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import {
getChatTitle,
getIsSavedDialog,
getSenderTitle,
isChatChannel,
isChatSuperGroup,
isUserId,
} from '../../global/helpers';
import {
selectAllowedMessageActionsSlow,
selectChat,
selectChatMessage,
selectChatMessages,
selectCurrentMiddleSearch,
selectForwardedSender,
selectIsChatBotNotStarted,
selectIsChatWithBot,
selectIsChatWithSelf,
selectIsInSelectMode,
selectIsRightColumnShown,
selectIsUserBlocked,
selectPinnedIds,
selectScheduledIds,
selectTabState,
@ -50,36 +33,27 @@ import {
selectThreadParam,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import cycleRestrict from '../../util/cycleRestrict';
import { getMessageKey } from '../../util/keys/messageKey';
import useAppLayout from '../../hooks/useAppLayout';
import useConnectionStatus from '../../hooks/useConnectionStatus';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useDerivedState from '../../hooks/useDerivedState';
import useElectronDrag from '../../hooks/useElectronDrag';
import useEnsureMessage from '../../hooks/useEnsureMessage';
import useLastCallback from '../../hooks/useLastCallback';
import useLongPress from '../../hooks/useLongPress';
import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
import useWindowSize from '../../hooks/window/useWindowSize';
import GroupCallTopPane from '../calls/group/GroupCallTopPane';
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 AudioPlayer from './AudioPlayer';
import ChatReportPanel from './ChatReportPanel';
import HeaderActions from './HeaderActions';
import HeaderPinnedMessage from './HeaderPinnedMessage';
import AudioPlayer from './panes/AudioPlayer';
import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
import './MiddleHeader.scss';
const ANIMATION_DURATION = 350;
const BACK_BUTTON_INACTIVE_TIME = 450;
const EMOJI_STATUS_SIZE = 22;
const SEARCH_LONGTAP_THRESHOLD = 500;
@ -89,7 +63,6 @@ type OwnProps = {
threadId: ThreadId;
messageListType: MessageListType;
isComments?: boolean;
isReady?: boolean;
isMobile?: boolean;
getCurrentPinnedIndex: Signal<number>;
getLoadingPinnedId: Signal<number | undefined>;
@ -98,11 +71,7 @@ type OwnProps = {
type StateProps = {
chat?: ApiChat;
pinnedMessageIds?: number[] | number;
messagesById?: Record<number, ApiMessage>;
canUnpin?: boolean;
isSavedDialog?: boolean;
topMessageSender?: ApiPeer;
typingStatus?: ApiTypingStatus;
isSelectModeActive?: boolean;
isLeftColumnShown?: boolean;
@ -110,61 +79,45 @@ type StateProps = {
audioMessage?: ApiMessage;
messagesCount?: number;
isChatWithSelf?: boolean;
hasButtonInHeader?: boolean;
shouldSkipHistoryAnimations?: boolean;
currentTransitionKey: number;
connectionState?: GlobalState['connectionState'];
isSyncing?: boolean;
isSynced?: boolean;
isFetchingDifference?: boolean;
emojiStatusSticker?: ApiSticker;
isMiddleSearchOpen?: boolean;
};
const MiddleHeader: FC<OwnProps & StateProps> = ({
chatId,
threadId,
messageListType,
isReady,
isMobile,
pinnedMessageIds,
messagesById,
canUnpin,
topMessageSender,
typingStatus,
isSelectModeActive,
isLeftColumnShown,
isRightColumnShown,
audioMessage,
chat,
messagesCount,
isComments,
isChatWithSelf,
hasButtonInHeader,
shouldSkipHistoryAnimations,
currentTransitionKey,
connectionState,
isSyncing,
isSynced,
isFetchingDifference,
getCurrentPinnedIndex,
getLoadingPinnedId,
emojiStatusSticker,
isSavedDialog,
isMiddleSearchOpen,
onFocusPinnedMessage,
}) => {
const {
openThreadWithInfo,
pinMessage,
focusMessage,
openChat,
openPreviousChat,
loadPinnedMessages,
toggleLeftColumn,
exitMessageSelectMode,
openPremiumModal,
openThread,
openStickerSet,
updateMiddleSearch,
} = getActions();
@ -173,32 +126,15 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const isBackButtonActive = useRef(true);
const { isTablet } = useAppLayout();
const currentPinnedIndex = useDerivedState(getCurrentPinnedIndex);
const pinnedMessageId = Array.isArray(pinnedMessageIds) ? pinnedMessageIds[currentPinnedIndex] : pinnedMessageIds;
const pinnedMessage = messagesById && pinnedMessageId ? messagesById[pinnedMessageId] : undefined;
const pinnedMessagesCount = Array.isArray(pinnedMessageIds)
? pinnedMessageIds.length : (pinnedMessageIds ? 1 : undefined);
const chatTitleLength = chat && getChatTitle(lang, chat).length;
const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined;
const { settings } = chat || {};
const isForum = chat?.isForum;
useEffect(() => {
if (isSynced && isReady && (threadId === MAIN_THREAD_ID || isForum)) {
loadPinnedMessages({ chatId, threadId });
}
}, [chatId, threadId, isSynced, isReady, isForum]);
useEnsureMessage(chatId, pinnedMessageId, pinnedMessage);
const { width: windowWidth } = useWindowSize();
const { isDesktop } = useAppLayout();
const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN;
const shouldShowCloseButton = isTablet && isLeftColumnShown;
// eslint-disable-next-line no-null/no-null
const componentRef = useRef<HTMLDivElement>(null);
const shouldAnimateTools = useRef<boolean>(true);
const handleOpenSearch = useLastCallback(() => {
updateMiddleSearch({ chatId, threadId, update: {} });
@ -222,27 +158,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
threshold: SEARCH_LONGTAP_THRESHOLD,
});
const handleUnpinMessage = useLastCallback((messageId: number) => {
pinMessage({ messageId, isUnpin: true });
});
const handlePinnedMessageClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>): void => {
const messageId = e.shiftKey && Array.isArray(pinnedMessageIds)
? pinnedMessageIds[cycleRestrict(pinnedMessageIds.length, pinnedMessageIds.indexOf(pinnedMessageId!) - 2)]
: pinnedMessageId!;
if (!getLoadingPinnedId()) {
focusMessage({
chatId, threadId, messageId, noForumTopicPanel: true,
});
onFocusPinnedMessage(messageId);
}
});
const handleAllPinnedClick = useLastCallback(() => {
openThread({ chatId, threadId, type: 'pinned' });
});
const setBackButtonActive = useLastCallback(() => {
setTimeout(() => {
isBackButtonActive.current = true;
@ -292,81 +207,14 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
setBackButtonActive();
});
const canToolsCollideWithChatInfo = (
windowWidth >= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN
&& windowWidth < SAFE_SCREEN_WIDTH_FOR_CHAT_INFO
) || (
windowWidth > MOBILE_SCREEN_MAX_WIDTH
&& windowWidth < MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN
&& (!chatTitleLength || chatTitleLength > 30)
);
const shouldUseStackedToolsClass = canToolsCollideWithChatInfo || (
windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
&& windowWidth < SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
);
const hasChatSettings = Boolean(settings?.canAddContact || settings?.canBlockContact || settings?.canReportSpam);
const {
shouldRender: shouldShowChatReportPanel,
transitionClassNames: chatReportPanelClassNames,
} = useShowTransitionDeprecated(hasChatSettings);
const renderingChatSettings = useCurrentOrPrev(hasChatSettings ? settings : undefined, true);
const {
shouldRender: shouldRenderAudioPlayer,
transitionClassNames: audioPlayerClassNames,
} = useShowTransitionDeprecated(Boolean(audioMessage));
const renderingAudioMessage = useCurrentOrPrev(audioMessage, true);
const {
shouldRender: shouldRenderPinnedMessage,
transitionClassNames: pinnedMessageClassNames,
} = useShowTransitionDeprecated(Boolean(pinnedMessage) && !isMiddleSearchOpen, undefined, true);
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
const renderingPinnedMessagesCount = useCurrentOrPrev(pinnedMessagesCount, true);
const renderingCanUnpin = useCurrentOrPrev(canUnpin, true);
const renderingPinnedMessageTitle = useCurrentOrPrev(topMessageTitle);
const prevTransitionKey = usePreviousDeprecated(currentTransitionKey);
const cleanupExceptionKey = (
prevTransitionKey !== undefined && prevTransitionKey < currentTransitionKey ? prevTransitionKey : undefined
);
const canRevealTools = (shouldRenderPinnedMessage && renderingPinnedMessage)
|| (shouldRenderAudioPlayer && renderingAudioMessage);
// Logic for transition to and from custom display of AudioPlayer/PinnedMessage on smaller screens
useLayoutEffect(() => {
const componentEl = componentRef.current;
if (!componentEl) {
return;
}
if (!shouldUseStackedToolsClass || !canRevealTools) {
componentEl.classList.remove('tools-stacked', 'animated');
shouldAnimateTools.current = true;
return;
}
if (isRightColumnShown || canToolsCollideWithChatInfo) {
if (shouldAnimateTools.current) {
componentEl.classList.add('tools-stacked', 'animated');
shouldAnimateTools.current = false;
}
// Remove animation class to prevent it messing up the show transitions
setTimeout(() => {
requestMutation(() => {
componentEl.classList.remove('animated');
});
}, ANIMATION_DURATION);
} else {
componentEl.classList.remove('tools-stacked');
shouldAnimateTools.current = true;
}
}, [shouldUseStackedToolsClass, canRevealTools, canToolsCollideWithChatInfo, isRightColumnShown]);
const isAudioPlayerActive = Boolean(audioMessage);
const isAudioPlayerRendering = isDesktop && isAudioPlayerActive;
const isPinnedMessagesFullWidth = isAudioPlayerActive || !isDesktop;
const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing || isFetchingDifference, true);
@ -470,10 +318,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
);
}
const isAudioPlayerRendered = Boolean(shouldRenderAudioPlayer && renderingAudioMessage);
const isPinnedMessagesFullWidth = isAudioPlayerRendered
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES);
useElectronDrag(componentRef);
return (
@ -486,56 +330,28 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
>
{renderInfo()}
</Transition>
{threadId === MAIN_THREAD_ID && !chat?.isForum && (
<GroupCallTopPane
hasPinnedOffset={
(shouldRenderPinnedMessage && Boolean(renderingPinnedMessage))
|| (shouldRenderAudioPlayer && Boolean(renderingAudioMessage))
}
chatId={chatId}
/>
)}
{shouldRenderPinnedMessage && renderingPinnedMessage && (
{!isPinnedMessagesFullWidth && (
<HeaderPinnedMessage
key={chatId}
message={renderingPinnedMessage}
count={renderingPinnedMessagesCount || 0}
index={currentPinnedIndex}
customTitle={renderingPinnedMessageTitle}
className={pinnedMessageClassNames}
onUnpinMessage={renderingCanUnpin ? handleUnpinMessage : undefined}
onClick={handlePinnedMessageClick}
onAllPinnedClick={handleAllPinnedClick}
getLoadingPinnedId={getLoadingPinnedId}
isFullWidth={isPinnedMessagesFullWidth}
/>
)}
{shouldShowChatReportPanel && (
<ChatReportPanel
key={chatId}
chatId={chatId}
settings={renderingChatSettings}
className={chatReportPanelClassNames}
threadId={threadId}
messageListType={messageListType}
onFocusPinnedMessage={onFocusPinnedMessage}
getLoadingPinnedId={getLoadingPinnedId}
getCurrentPinnedIndex={getCurrentPinnedIndex}
/>
)}
<div className="header-tools">
{isAudioPlayerRendered && (
<AudioPlayer
key={getMessageKey(renderingAudioMessage!)}
message={renderingAudioMessage!}
className={audioPlayerClassNames}
/>
{isAudioPlayerRendering && (
<AudioPlayer />
)}
<HeaderActions
chatId={chatId}
threadId={threadId}
messageListType={messageListType}
isMobile={isMobile}
canExpandActions={!isAudioPlayerRendered}
canExpandActions={!isAudioPlayerRendering}
/>
</div>
</div>
@ -568,23 +384,14 @@ export default memo(withGlobal<OwnProps>(
messagesCount = threadInfo?.messagesCount || 0;
}
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
const isChatWithBot = chat && selectIsChatWithBot(global, chat);
const canRestartBot = Boolean(isChatWithBot && selectIsUserBlocked(global, chatId));
const canStartBot = isChatWithBot && !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
const canSubscribe = Boolean(
chat && (isMainThread || chat.isForum) && (isChatChannel(chat) || isChatSuperGroup(chat)) && chat.isNotJoined,
);
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
const typingStatus = selectThreadParam(global, chatId, threadId, 'typingStatus');
const emojiStatus = chat?.emojiStatus;
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const isMiddleSearchOpen = Boolean(selectCurrentMiddleSearch(global));
const state: StateProps = {
return {
typingStatus,
isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
@ -597,52 +404,9 @@ export default memo(withGlobal<OwnProps>(
currentTransitionKey: Math.max(0, messageLists.length - 1),
connectionState: global.connectionState,
isSyncing: global.isSyncing,
isSynced: global.isSynced,
isFetchingDifference: global.isFetchingDifference,
emojiStatusSticker,
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
isSavedDialog,
isMiddleSearchOpen,
};
const messagesById = selectChatMessages(global, chatId);
if (messageListType !== 'thread' || !messagesById) {
return state;
}
if (threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum) {
const pinnedMessageId = Number(threadId);
const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined;
const topMessageSender = message ? selectForwardedSender(global, message) : undefined;
return {
...state,
pinnedMessageIds: pinnedMessageId,
messagesById,
canUnpin: false,
topMessageSender,
};
}
const pinnedMessageIds = !isSavedDialog ? selectPinnedIds(global, chatId, threadId) : undefined;
if (pinnedMessageIds?.length) {
const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
const {
canUnpin = false,
} = (
firstPinnedMessage
&& pinnedMessageIds.length === 1
&& selectAllowedMessageActionsSlow(global, firstPinnedMessage, threadId)
) || {};
return {
...state,
pinnedMessageIds,
messagesById,
canUnpin,
};
}
return state;
},
)(MiddleHeader));

View File

@ -0,0 +1,36 @@
.root {
position: absolute;
top: 3.5rem;
left: 0;
width: 100%;
z-index: var(--z-middle-header);
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
transition: width var(--layer-transition);
@media (min-width: 1276px) {
:global(#Main.right-column-open) & {
width: calc(100% - var(--right-column-width));
}
}
/* stylelint-disable-next-line plugin/selector-tag-no-without-class */
> div:last-child {
box-shadow: 0 2px 2px var(--color-light-shadow);
}
// In case if there are no children, we need to have a shadow
&::before {
content: '';
position: absolute;
pointer-events: none;
top: -2px;
height: 2px;
left: 0;
right: 0;
box-shadow: 0 2px 2px var(--color-light-shadow);
z-index: -100; // Hide behind the children
}
}

View File

@ -0,0 +1,163 @@
import React, {
memo, useRef, useSignal,
} from '../../lib/teact/teact';
import { setExtraStyles } from '../../lib/teact/teact-dom';
import { withGlobal } from '../../global';
import type { MessageListType } from '../../global/types';
import type { ThreadId } from '../../types';
import type { Signal } from '../../util/signals';
import { type ApiChat, MAIN_THREAD_ID } from '../../api/types';
import {
selectChat, selectChatMessage, selectCurrentMiddleSearch, selectTabState,
} from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
import useAppLayout from '../../hooks/useAppLayout';
import useEffectOnce from '../../hooks/useEffectOnce';
import useShowTransition from '../../hooks/useShowTransition';
import { useSignalEffect } from '../../hooks/useSignalEffect';
import { applyAnimationState, type PaneState } from './hooks/useHeaderPane';
import GroupCallTopPane from '../calls/group/GroupCallTopPane';
import AudioPlayer from './panes/AudioPlayer';
import ChatReportPane from './panes/ChatReportPane';
import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
import styles from './MiddleHeaderPanes.module.scss';
type OwnProps = {
className?: string;
chatId: string;
threadId: ThreadId;
messageListType: MessageListType;
getCurrentPinnedIndex: Signal<number>;
getLoadingPinnedId: Signal<number | undefined>;
onFocusPinnedMessage: (messageId: number) => void;
};
type StateProps = {
chat?: ApiChat;
isAudioPlayerRendered?: boolean;
isMiddleSearchOpen?: boolean;
};
const FALLBACK_PANE_STATE = { height: 0 };
const MiddleHeaderPanes = ({
className,
chatId,
threadId,
messageListType,
chat,
getCurrentPinnedIndex,
getLoadingPinnedId,
isAudioPlayerRendered,
isMiddleSearchOpen,
onFocusPinnedMessage,
}: OwnProps & StateProps) => {
const { settings } = chat || {};
const { isDesktop } = useAppLayout();
const [getAudioPlayerState, setAudioPlayerState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const [getPinnedState, setPinnedState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const [getGroupCallState, setGroupCallState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const [getChatReportState, setChatReportState] = useSignal<PaneState>(FALLBACK_PANE_STATE);
const isPinnedMessagesFullWidth = isAudioPlayerRendered || !isDesktop;
const isFirstRenderRef = useRef(true);
const {
shouldRender,
ref,
} = useShowTransition({
isOpen: !isMiddleSearchOpen,
withShouldRender: true,
});
useEffectOnce(() => {
isFirstRenderRef.current = false;
});
useSignalEffect(() => {
const audioPlayerState = getAudioPlayerState();
const pinnedState = getPinnedState();
const groupCallState = getGroupCallState();
const chatReportState = getChatReportState();
// Keep in sync with the order of the panes in the DOM
const stateArray = [audioPlayerState, groupCallState, chatReportState, pinnedState];
const isFirstRender = isFirstRenderRef.current;
const totalHeight = stateArray.reduce((acc, state) => acc + state.height, 0);
const middleColumn = document.getElementById('MiddleColumn');
if (!middleColumn) return;
applyAnimationState(stateArray, isFirstRender);
setExtraStyles(middleColumn, {
'--middle-header-panes-height': `${totalHeight}px`,
});
}, [getAudioPlayerState, getGroupCallState, getPinnedState, getChatReportState]);
if (!shouldRender) return undefined;
return (
<div ref={ref} className={buildClassName(styles.root, className)}>
<AudioPlayer
isFullWidth
onPaneStateChange={setAudioPlayerState}
isHidden={isDesktop}
/>
{threadId === MAIN_THREAD_ID && !chat?.isForum && (
<GroupCallTopPane
chatId={chatId}
onPaneStateChange={setGroupCallState}
/>
)}
<ChatReportPane
chatId={chatId}
canAddContact={settings?.canAddContact}
canBlockContact={settings?.canBlockContact}
canReportSpam={settings?.canReportSpam}
isAutoArchived={settings?.isAutoArchived}
onPaneStateChange={setChatReportState}
/>
<HeaderPinnedMessage
chatId={chatId}
threadId={threadId}
messageListType={messageListType}
onFocusPinnedMessage={onFocusPinnedMessage}
getLoadingPinnedId={getLoadingPinnedId}
getCurrentPinnedIndex={getCurrentPinnedIndex}
onPaneStateChange={setPinnedState}
isFullWidth
shouldHide={!isPinnedMessagesFullWidth}
/>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, {
chatId,
}): StateProps => {
const { audioPlayer } = selectTabState(global);
const chat = selectChat(global, chatId);
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
const audioMessage = audioChatId && audioMessageId
? selectChatMessage(global, audioChatId, audioMessageId)
: undefined;
const isMiddleSearchOpen = Boolean(selectCurrentMiddleSearch(global));
return {
chat,
isAudioPlayerRendered: Boolean(audioMessage),
isMiddleSearchOpen,
};
},
)(MiddleHeaderPanes));

View File

@ -1,6 +1,7 @@
import React, { type TeactNode } from '../../../../lib/teact/teact';
import type { ApiKeyboardButton } from '../../../../api/types';
import type { LangFn } from '../../../../util/localization';
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { replaceWithTeact } from '../../../../util/replaceWithTeact';
@ -10,7 +11,7 @@ import { type OldLangFn } from '../../../../hooks/useOldLang';
import Icon from '../../../common/icons/Icon';
export default function renderKeyboardButtonText(lang: OldLangFn, button: ApiKeyboardButton): TeactNode {
export default function renderKeyboardButtonText(lang: OldLangFn | LangFn, button: ApiKeyboardButton): TeactNode {
if (button.type === 'receipt') {
return lang('PaymentReceipt');
}

View File

@ -0,0 +1,133 @@
import {
type RefObject,
useEffect,
useLayoutEffect,
useRef,
useState,
useUnmountCleanup,
} from '../../../lib/teact/teact';
import { setExtraStyles } from '../../../lib/teact/teact-dom';
import { requestForcedReflow, requestNextMutation } from '../../../lib/fasterdom/fasterdom';
import useTimeout from '../../../hooks/schedulers/useTimeout';
import useLastCallback from '../../../hooks/useLastCallback';
export interface PaneState {
element?: HTMLElement;
height: number;
isOpen?: boolean;
}
// Max slide transition duration
const CLOSE_DURATION = 450;
export default function useHeaderPane<RefType extends HTMLElement = HTMLDivElement>({
ref: providedRef,
isOpen,
isDisabled,
onStateChange,
} : {
ref?: RefObject<RefType | null>;
isOpen?: boolean;
isDisabled?: boolean;
onStateChange?: (state: PaneState) => void;
}) {
const [shouldRender, setShouldRender] = useState(true);
// eslint-disable-next-line no-null/no-null
const localRef = useRef<RefType>(null);
const ref = providedRef || localRef;
const reset = useLastCallback(() => {
setShouldRender(true);
onStateChange?.({
element: undefined,
height: 0,
isOpen: false,
});
});
useEffect(() => {
if (isDisabled) {
reset();
}
}, [isDisabled]);
useEffect(() => {
if (isOpen) {
setShouldRender(true);
}
}, [isOpen]);
useUnmountCleanup(reset);
useTimeout(() => {
setShouldRender(false);
}, !isOpen ? CLOSE_DURATION : undefined);
useLayoutEffect(() => {
const element = ref.current;
if (isDisabled || !element || !shouldRender) return;
if (!isOpen) {
onStateChange?.({
element,
height: 0,
isOpen: false,
});
return;
}
requestForcedReflow(() => {
const currentHeight = element.offsetHeight;
return () => {
onStateChange?.({
element,
height: currentHeight,
isOpen,
});
};
});
}, [isOpen, shouldRender, isDisabled, ref, onStateChange]);
return {
shouldRender,
ref,
};
}
export function applyAnimationState(list: PaneState[], noTransition = false) {
let cumulativeHeight = 0;
for (let i = 0; i < list.length; i++) {
const state = list[i];
const element = state.element;
if (!element) {
continue;
}
const shiftPx = `${cumulativeHeight}px`;
const apply = () => {
setExtraStyles(element, {
transform: `translateY(${state.isOpen ? shiftPx : `calc(${shiftPx} - 100%)`})`,
zIndex: String(-i),
transition: noTransition ? 'none' : '',
});
};
if (!element.dataset.isPanelOpen && state.isOpen && !noTransition) {
// Start animation right above its final position
setExtraStyles(element, {
transform: `translateY(calc(${shiftPx} - 100%))`,
zIndex: String(-i),
transition: 'none',
});
element.dataset.isPanelOpen = 'true';
requestNextMutation(apply);
} else {
apply();
}
cumulativeHeight += state.height;
}
}

View File

@ -6,7 +6,6 @@ import useRunDebounced from '../../../hooks/useRunDebounced';
const DEBOUNCE = 1000;
const STICKY_TOP = 10;
const STICKY_TOP_WITH_TOOLS = 60;
export default function useStickyDates() {
// For some reason we can not synchronously hide a sticky element (from `useLayoutEffect`) when chat opens
@ -15,7 +14,7 @@ export default function useStickyDates() {
const runDebounced = useRunDebounced(DEBOUNCE, true);
const updateStickyDates = useLastCallback((container: HTMLDivElement, hasTools?: boolean) => {
const updateStickyDates = useLastCallback((container: HTMLDivElement) => {
markIsScrolled();
if (!document.body.classList.contains('is-scrolling-messages')) {
@ -25,7 +24,7 @@ export default function useStickyDates() {
}
runDebounced(() => {
const stuckDateEl = findStuckDate(container, hasTools);
const stuckDateEl = findStuckDate(container);
if (stuckDateEl) {
requestMutation(() => {
stuckDateEl.classList.add('stuck');
@ -49,13 +48,16 @@ export default function useStickyDates() {
};
}
function findStuckDate(container: HTMLElement, hasTools?: boolean) {
function findStuckDate(container: HTMLElement) {
const allElements = container.querySelectorAll<HTMLDivElement>('.sticky-date');
const containerTop = container.scrollTop;
const computedStyle = getComputedStyle(container);
const headerActionsHeight = parseInt(computedStyle.getPropertyValue('--middle-header-panes-height'), 10);
return Array.from(allElements).find((el) => {
const { offsetTop, offsetHeight } = el;
const top = offsetTop - containerTop;
return -offsetHeight <= top && top <= (hasTools ? STICKY_TOP_WITH_TOOLS : STICKY_TOP);
return -offsetHeight <= top && top <= (headerActionsHeight || STICKY_TOP);
});
}

View File

@ -406,7 +406,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
});
const handleUnpin = useLastCallback(() => {
pinMessage({ messageId: message.id, isUnpin: true });
pinMessage({ chatId: message.chatId, messageId: message.id, isUnpin: true });
closeMenu();
});

View File

@ -312,10 +312,10 @@ const MessageContextMenu: FC<OwnProps> = ({
const getLayout = useLastCallback(() => {
const extraHeightAudioPlayer = (isMobile
&& (document.querySelector<HTMLElement>('.AudioPlayer-content'))?.offsetHeight) || 0;
const pinnedElement = document.querySelector<HTMLElement>('.HeaderPinnedMessageWrapper');
const extraHeightPinned = (((isMobile && !extraHeightAudioPlayer)
|| (!isMobile && pinnedElement?.classList.contains('full-width')))
&& pinnedElement?.offsetHeight) || 0;
const middleColumn = document.getElementById('MiddleColumn')!;
const middleColumnComputedStyle = getComputedStyle(middleColumn);
const headerToolsHeight = parseFloat(middleColumnComputedStyle.getPropertyValue('--middle-header-panes-height'));
const extraHeightPinned = headerToolsHeight || 0;
return {
extraPaddingX: SCROLLBAR_WIDTH,

View File

@ -1,8 +1,9 @@
@use "../../../styles/mixins";
.AudioPlayer {
display: flex;
align-items: center;
margin-left: auto;
margin-top: -0.25rem;
margin-bottom: -0.25rem;
body.no-page-transitions & {
transition: none !important;
@ -170,10 +171,6 @@
max-width: 10rem;
}
@media (max-width: 600px) {
max-width: 100%;
}
@media (min-width: 1440px) {
max-width: 24rem;
.right-column-shown & {
@ -213,6 +210,15 @@
}
}
&.full-width-player {
@include mixins.header-pane;
.AudioPlayer-content {
max-width: none;
flex-grow: 1;
}
}
.playback-rate-menu .bubble {
min-width: auto;

View File

@ -1,47 +1,54 @@
import type { FC } from '../../lib/teact/teact';
import React, { useMemo, useRef } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { FC } from '../../../lib/teact/teact';
import React, { useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type {
ApiAudio, ApiChat, ApiMessage, ApiPeer,
} from '../../api/types';
import type { AudioOrigin } from '../../types';
MediaContent,
} from '../../../api/types';
import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../config';
import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../../config';
import {
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal,
} from '../../global/helpers';
import { selectChat, selectSender, selectTabState } from '../../global/selectors';
import { makeTrackId } from '../../util/audioPlayer';
import buildClassName from '../../util/buildClassName';
import * as mediaLoader from '../../util/mediaLoader';
import { clearMediaSession } from '../../util/mediaSession';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment';
import renderText from '../common/helpers/renderText';
} from '../../../global/helpers';
import {
selectChat, selectChatMessage, selectSender, selectTabState,
} from '../../../global/selectors';
import { makeTrackId } from '../../../util/audioPlayer';
import buildClassName from '../../../util/buildClassName';
import * as mediaLoader from '../../../util/mediaLoader';
import { clearMediaSession } from '../../../util/mediaSession';
import { IS_IOS, IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import renderText from '../../common/helpers/renderText';
import useAppLayout from '../../hooks/useAppLayout';
import useAudioPlayer from '../../hooks/useAudioPlayer';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
import useLastCallback from '../../hooks/useLastCallback';
import useMessageMediaMetadata from '../../hooks/useMessageMediaMetadata';
import useOldLang from '../../hooks/useOldLang';
import useAppLayout from '../../../hooks/useAppLayout';
import useAudioPlayer from '../../../hooks/useAudioPlayer';
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLastCallback from '../../../hooks/useLastCallback';
import useMessageMediaMetadata from '../../../hooks/useMessageMediaMetadata';
import useOldLang from '../../../hooks/useOldLang';
import useShowTransition from '../../../hooks/useShowTransition';
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
import Button from '../ui/Button';
import DropdownMenu from '../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem';
import RangeSlider from '../ui/RangeSlider';
import RippleEffect from '../ui/RippleEffect';
import Button from '../../ui/Button';
import DropdownMenu from '../../ui/DropdownMenu';
import MenuItem from '../../ui/MenuItem';
import RangeSlider from '../../ui/RangeSlider';
import RippleEffect from '../../ui/RippleEffect';
import './AudioPlayer.scss';
type OwnProps = {
message: ApiMessage;
origin?: AudioOrigin;
className?: string;
noUi?: boolean;
isFullWidth?: boolean;
isHidden?: boolean;
onPaneStateChange?: (state: PaneState) => void;
};
type StateProps = {
message?: ApiMessage;
sender?: ApiPeer;
chat?: ApiChat;
volume: number;
@ -72,6 +79,8 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
playbackRate,
isPlaybackRateActive,
isMuted,
isFullWidth,
onPaneStateChange,
}) => {
const {
setAudioPlayerVolume,
@ -81,16 +90,19 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
closeAudioPlayer,
} = getActions();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const lang = useOldLang();
const { isMobile } = useAppLayout();
const { audio, voice, video } = getMessageContent(message);
const renderingMessage = useCurrentOrPrev(message);
const { audio, voice, video } = renderingMessage ? getMessageContent(renderingMessage) : {} satisfies MediaContent;
const isVoice = Boolean(voice || video);
const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION;
const senderName = sender ? getSenderTitle(lang, sender) : undefined;
const mediaData = mediaLoader.getFromMemory(getMessageMediaHash(message, 'inline')!) as (string | undefined);
const mediaMetadata = useMessageMediaMetadata(message, sender, chat);
const mediaHash = renderingMessage && getMessageMediaHash(renderingMessage, 'inline');
const mediaData = mediaHash && mediaLoader.getFromMemory(mediaHash);
const mediaMetadata = useMessageMediaMetadata(renderingMessage, sender, chat);
const {
playPause,
@ -104,8 +116,8 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
toggleMuted,
setPlaybackRate,
} = useAudioPlayer(
makeTrackId(message),
getMediaDuration(message)!,
message && makeTrackId(message),
message ? getMediaDuration(message)! : 0,
isVoice ? 'voice' : 'audio',
mediaData,
undefined,
@ -114,18 +126,34 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
true,
undefined,
undefined,
isMessageLocal(message),
message && isMessageLocal(message),
true,
);
const isOpen = Boolean(message);
const {
ref: transitionRef,
} = useShowTransition({
isOpen,
shouldForceOpen: isFullWidth, // Use pane animation instead
});
const { ref, shouldRender } = useHeaderPane({
isOpen,
isDisabled: !isFullWidth,
ref: transitionRef,
onStateChange: onPaneStateChange,
});
const {
isContextMenuOpen,
handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref);
} = useContextMenuHandlers(transitionRef, !shouldRender);
const handleClick = useLastCallback(() => {
focusMessage({ chatId: message.chatId, messageId: message.id });
const { chatId, id } = renderingMessage!;
focusMessage({ chatId, messageId: id });
});
const handleClose = useLastCallback(() => {
@ -221,12 +249,16 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
return 'icon-volume-3';
}, [volume, isMuted]);
if (noUi) {
if (noUi || !shouldRender) {
return undefined;
}
return (
<div className={buildClassName('AudioPlayer', className)} dir={lang.isRtl ? 'rtl' : undefined} ref={ref}>
<div
className={buildClassName('AudioPlayer', isFullWidth ? 'full-width-player' : 'mini-player', className)}
dir={lang.isRtl ? 'rtl' : undefined}
ref={ref}
>
<div className="AudioPlayer-content" onClick={handleClick}>
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
<RippleEffect />
@ -365,14 +397,19 @@ function renderPlaybackRateMenuItem(
}
export default withGlobal<OwnProps>(
(global, { message }): StateProps => {
const sender = selectSender(global, message);
const chat = selectChat(global, message.chatId);
(global, { isHidden }): StateProps => {
const { audioPlayer } = selectTabState(global);
const { chatId, messageId } = audioPlayer;
const message = !isHidden && chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
const sender = message && selectSender(global, message);
const chat = message && selectChat(global, message.chatId);
const {
volume, playbackRate, isMuted, isPlaybackRateActive,
} = selectTabState(global).audioPlayer;
return {
message,
sender,
chat,
volume,

View File

@ -0,0 +1,20 @@
@use "../../../styles/mixins";
.ChatReportPane {
@include mixins.header-pane;
display: flex;
align-items: center;
body.no-page-transitions & {
.ripple-container {
display: none;
}
}
&--Button {
margin-left: 0.25rem;
flex: 1 1 50%;
white-space: nowrap;
}
}

View File

@ -1,29 +1,36 @@
import type { FC } from '../../lib/teact/teact';
import React, { memo, useState } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global';
import type { FC } from '../../../lib/teact/teact';
import React, { memo, useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiChatSettings, ApiUser } from '../../api/types';
import type { ApiChat, ApiUser } from '../../../api/types';
import {
getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup,
} from '../../global/helpers';
import { selectChat, selectUser } from '../../global/selectors';
import buildClassName from '../../util/buildClassName';
} from '../../../global/helpers';
import { selectChat, selectUser } from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import useFlag from '../../hooks/useFlag';
import useLastCallback from '../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useFlag from '../../../hooks/useFlag';
import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
import Button from '../ui/Button';
import Checkbox from '../ui/Checkbox';
import ConfirmDialog from '../ui/ConfirmDialog';
import Icon from '../../common/icons/Icon';
import Button from '../../ui/Button';
import Checkbox from '../../ui/Checkbox';
import ConfirmDialog from '../../ui/ConfirmDialog';
import './ChatReportPanel.scss';
import './ChatReportPane.scss';
type OwnProps = {
chatId: string;
className?: string;
settings?: ApiChatSettings;
isAutoArchived?: boolean;
canReportSpam?: boolean;
canAddContact?: boolean;
canBlockContact?: boolean;
onPaneStateChange?: (state: PaneState) => void;
};
type StateProps = {
@ -32,8 +39,17 @@ type StateProps = {
user?: ApiUser;
};
const ChatReportPanel: FC<OwnProps & StateProps> = ({
chatId, className, chat, user, settings, currentUserId,
const ChatReportPane: FC<OwnProps & StateProps> = ({
chatId,
className,
isAutoArchived,
canReportSpam,
canAddContact,
canBlockContact,
chat,
user,
currentUserId,
onPaneStateChange,
}) => {
const {
openAddContactDialog,
@ -44,21 +60,23 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
deleteChatUser,
deleteHistory,
toggleChatArchived,
hideChatReportPanel,
hideChatReportPane,
} = getActions();
const lang = useOldLang();
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
const [shouldReportSpam, setShouldReportSpam] = useState<boolean>(true);
const [shouldDeleteChat, setShouldDeleteChat] = useState<boolean>(true);
const {
isAutoArchived, canReportSpam, canAddContact, canBlockContact,
} = settings || {};
const isBasicGroup = chat && isChatBasicGroup(chat);
const renderingCanAddContact = useCurrentOrPrev(canAddContact);
const renderingCanBlockContact = useCurrentOrPrev(canBlockContact);
const renderingCanReportSpam = useCurrentOrPrev(canReportSpam);
const renderingIsAutoArchived = useCurrentOrPrev(isAutoArchived);
const handleAddContact = useLastCallback(() => {
openAddContactDialog({ userId: chatId });
if (isAutoArchived) {
if (renderingIsAutoArchived) {
toggleChatArchived({ id: chatId });
}
});
@ -66,7 +84,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
const handleConfirmBlock = useLastCallback(() => {
closeBlockUserModal();
blockUser({ userId: chatId });
if (canReportSpam && shouldReportSpam) {
if (renderingCanReportSpam && shouldReportSpam) {
reportSpam({ chatId });
}
if (shouldDeleteChat) {
@ -74,8 +92,8 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
}
});
const handleCloseReportPanel = useLastCallback(() => {
hideChatReportPanel({ chatId });
const handleCloseReportPane = useLastCallback(() => {
hideChatReportPane({ chatId });
});
const handleChatReportSpam = useLastCallback(() => {
@ -89,42 +107,53 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
}
});
if (!settings || (!chat && !user)) {
return undefined;
}
const hasAnyButton = canAddContact || canBlockContact || canReportSpam;
const isRendering = Boolean(hasAnyButton && (chat || user));
const { ref, shouldRender } = useHeaderPane({
isOpen: isRendering,
onStateChange: onPaneStateChange,
});
if (!shouldRender) return undefined;
return (
<div className={buildClassName('ChatReportPanel', className)} dir={lang.isRtl ? 'rtl' : undefined}>
{canAddContact && (
<div
ref={ref}
className={buildClassName('ChatReportPane', className)}
dir={lang.isRtl ? 'rtl' : undefined}
>
{renderingCanAddContact && (
<Button
isText
fluid
size="tiny"
className="UserReportPanel--Button"
className="ChatReportPane--Button"
onClick={handleAddContact}
>
{lang('lng_new_contact_add')}
</Button>
)}
{canBlockContact && (
{renderingCanBlockContact && (
<Button
color="danger"
isText
fluid
size="tiny"
className="UserReportPanel--Button"
className="ChatReportPane--Button"
onClick={openBlockUserModal}
>
{lang('lng_new_contact_block')}
</Button>
)}
{canReportSpam && !canBlockContact && (
{renderingCanReportSpam && !renderingCanBlockContact && (
<Button
color="danger"
isText
fluid
size="tiny"
className="UserReportPanel--Button"
className="ChatReportPane--Button"
onClick={openBlockUserModal}
>
{lang('lng_report_spam_and_leave')}
@ -133,12 +162,12 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
<Button
round
ripple
size="tiny"
size="smaller"
color="translucent"
onClick={handleCloseReportPanel}
onClick={handleCloseReportPane}
ariaLabel={lang('Close')}
>
<i className="icon icon-close" />
<Icon name="close" />
</Button>
<ConfirmDialog
isOpen={isBlockUserModalOpen}
@ -176,4 +205,4 @@ export default memo(withGlobal<OwnProps>(
chat: selectChat(global, chatId),
user: selectUser(global, chatId),
}),
)(ChatReportPanel));
)(ChatReportPane));

View File

@ -1,4 +1,4 @@
@use "../../styles/mixins";
@use "../../../styles/mixins";
.root {
display: flex;
@ -26,6 +26,12 @@
transition: none !important;
}
> :global(.Button) {
flex-shrink: 0;
}
}
.mini {
@media (min-width: 1276px) {
transform: translate3d(0, 0, 0);
transition: opacity 0.15s ease, transform var(--layer-transition);
@ -38,33 +44,10 @@
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
}
}
> :global(.Button) {
flex-shrink: 0;
}
}
.root:global(.full-width) {
position: absolute;
left: 0;
right: 0;
top: 100%;
background: var(--color-background);
padding: 0.25rem 0.8125rem 0.25rem 1rem;
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
transform: translate3d(0, 0, 0);
transition: opacity 0.15s ease, transform var(--layer-transition);
&::before {
content: "";
display: block;
position: absolute;
top: -0.1875rem;
left: 0;
right: 0;
height: 0.125rem;
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
}
.fullWidth {
@include mixins.header-pane;
.pinnedMessage {
margin-top: 0;
@ -75,12 +58,6 @@
.messageText {
max-width: none;
}
@media (min-width: 1276px) {
:global(#Main.right-column-open) & {
padding-left: calc(var(--right-column-width) + 1rem);
}
}
}
.loading {
@ -244,30 +221,4 @@
max-width: none;
}
}
.root:global(.full-width) {
display: none;
}
.root {
@include mixins.header-mobile();
}
}
@media (min-width: 1276px) and (max-width: 1439px) {
:global(.MiddleHeader:not(.tools-stacked)) .root {
opacity: 1;
:global(#Main.right-column-open) & {
opacity: 0;
}
}
}
:global(.tools-stacked.animated) .root {
animation: fade-in var(--layer-transition) forwards;
:global(body.no-page-transitions) & {
animation: none;
}
}

View File

@ -0,0 +1,395 @@
import React, { memo, useEffect } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiMessage, ApiPeer } from '../../../api/types';
import type { MessageListType } from '../../../global/types';
import type { ThreadId } from '../../../types';
import type { Signal } from '../../../util/signals';
import { MAIN_THREAD_ID } from '../../../api/types';
import {
getIsSavedDialog,
getMessageIsSpoiler,
getMessageMediaHash,
getMessageSingleInlineButton,
getMessageVideo,
getSenderTitle,
} from '../../../global/helpers';
import {
selectAllowedMessageActionsSlow,
selectChat,
selectChatMessage,
selectChatMessages,
selectForwardedSender,
selectPinnedIds,
} from '../../../global/selectors';
import buildClassName from '../../../util/buildClassName';
import cycleRestrict from '../../../util/cycleRestrict';
import { IS_TOUCH_ENV } from '../../../util/windowEnvironment';
import { getPictogramDimensions, REM } from '../../common/helpers/mediaDimensions';
import renderText from '../../common/helpers/renderText';
import renderKeyboardButtonText from '../composer/helpers/renderKeyboardButtonText';
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useDerivedState from '../../../hooks/useDerivedState';
import useEnsureMessage from '../../../hooks/useEnsureMessage';
import { useFastClick } from '../../../hooks/useFastClick';
import useFlag from '../../../hooks/useFlag';
import useLang from '../../../hooks/useLang';
import useLastCallback from '../../../hooks/useLastCallback';
import useMedia from '../../../hooks/useMedia';
import useShowTransition from '../../../hooks/useShowTransition';
import useThumbnail from '../../../hooks/useThumbnail';
import useAsyncRendering from '../../right/hooks/useAsyncRendering';
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
import AnimatedCounter from '../../common/AnimatedCounter';
import Icon from '../../common/icons/Icon';
import MediaSpoiler from '../../common/MediaSpoiler';
import MessageSummary from '../../common/MessageSummary';
import Button from '../../ui/Button';
import ConfirmDialog from '../../ui/ConfirmDialog';
import RippleEffect from '../../ui/RippleEffect';
import Spinner from '../../ui/Spinner';
import Transition from '../../ui/Transition';
import PinnedMessageNavigation from '../PinnedMessageNavigation';
import styles from './HeaderPinnedMessage.module.scss';
const SHOW_LOADER_DELAY = 450;
const EMOJI_SIZE = 1.125 * REM;
type OwnProps = {
chatId: string;
threadId: ThreadId;
// eslint-disable-next-line react/no-unused-prop-types
messageListType: MessageListType;
className?: string;
isFullWidth?: boolean;
shouldHide?: boolean;
getLoadingPinnedId: Signal<number | undefined>;
getCurrentPinnedIndex: Signal<number>;
onFocusPinnedMessage: (messageId: number) => void;
onPaneStateChange?: (state: PaneState) => void;
};
type StateProps = {
chat?: ApiChat;
pinnedMessageIds?: number[] | number;
messagesById?: Record<number, ApiMessage>;
canUnpin?: boolean;
topMessageSender?: ApiPeer;
isSynced?: boolean;
};
const HeaderPinnedMessage = ({
chatId,
threadId,
canUnpin,
getLoadingPinnedId,
pinnedMessageIds,
messagesById,
isFullWidth,
topMessageSender,
getCurrentPinnedIndex,
className,
chat,
isSynced,
shouldHide,
onPaneStateChange,
onFocusPinnedMessage,
}: OwnProps & StateProps) => {
const {
clickBotInlineButton, focusMessage, openThread, pinMessage, loadPinnedMessages,
} = getActions();
const lang = useLang();
const currentPinnedIndex = useDerivedState(getCurrentPinnedIndex);
const pinnedMessageId = Array.isArray(pinnedMessageIds) ? pinnedMessageIds[currentPinnedIndex] : pinnedMessageIds;
const pinnedMessage = messagesById && pinnedMessageId ? messagesById[pinnedMessageId] : undefined;
const pinnedMessagesCount = Array.isArray(pinnedMessageIds)
? pinnedMessageIds.length : (pinnedMessageIds ? 1 : 0);
const pinnedMessageNumber = Math.max(pinnedMessagesCount - currentPinnedIndex, 1);
const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined;
const video = pinnedMessage && getMessageVideo(pinnedMessage);
const gif = video?.isGif ? video : undefined;
const isVideoThumbnail = Boolean(gif && !gif.previewPhotoSizes?.length);
const mediaThumbnail = useThumbnail(pinnedMessage);
const mediaHash = pinnedMessage && getMessageMediaHash(pinnedMessage, isVideoThumbnail ? 'full' : 'pictogram');
const mediaBlobUrl = useMedia(mediaHash);
const isSpoiler = pinnedMessage && getMessageIsSpoiler(pinnedMessage);
const isLoading = Boolean(useDerivedState(getLoadingPinnedId));
const canRenderLoader = useAsyncRendering([isLoading], SHOW_LOADER_DELAY);
const shouldShowLoader = canRenderLoader && isLoading;
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
useEffect(() => {
if (isSynced && (threadId === MAIN_THREAD_ID || chat?.isForum)) {
loadPinnedMessages({ chatId, threadId });
}
}, [chatId, threadId, isSynced, chat]);
useEnsureMessage(chatId, pinnedMessageId, pinnedMessage);
const isOpen = Boolean(pinnedMessage) && !shouldHide;
const {
ref: transitionRef,
} = useShowTransition({
isOpen,
noOpenTransition: true,
shouldForceOpen: isFullWidth, // Use pane animation instead
});
const { ref, shouldRender } = useHeaderPane({
isOpen,
isDisabled: !isFullWidth,
ref: transitionRef,
onStateChange: onPaneStateChange,
});
const [isUnpinDialogOpen, openUnpinDialog, closeUnpinDialog] = useFlag();
const handleUnpinMessage = useLastCallback(() => {
closeUnpinDialog();
pinMessage({ chatId, messageId: pinnedMessage!.id, isUnpin: true });
});
const inlineButton = pinnedMessage && getMessageSingleInlineButton(pinnedMessage);
const handleInlineButtonClick = useLastCallback(() => {
if (inlineButton) {
clickBotInlineButton({ chatId: pinnedMessage.chatId, messageId: pinnedMessage.id, button: inlineButton });
}
});
const handleAllPinnedClick = useLastCallback(() => {
openThread({ chatId, threadId, type: 'pinned' });
});
const handleMessageClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>): void => {
const nextMessageId = e.shiftKey && Array.isArray(pinnedMessageIds)
? pinnedMessageIds[cycleRestrict(pinnedMessageIds.length, pinnedMessageIds.indexOf(pinnedMessageId!) - 2)]
: pinnedMessageId!;
if (!getLoadingPinnedId()) {
focusMessage({
chatId, threadId, messageId: nextMessageId, noForumTopicPanel: true,
});
onFocusPinnedMessage(nextMessageId);
}
});
const [noHoverColor, markNoHoverColor, unmarkNoHoverColor] = useFlag();
const { handleClick, handleMouseDown } = useFastClick(handleMessageClick);
function renderPictogram(thumbDataUri?: string, blobUrl?: string, isFullVideo?: boolean, asSpoiler?: boolean) {
const { width, height } = getPictogramDimensions();
const srcUrl = blobUrl || thumbDataUri;
const shouldRenderVideo = isFullVideo && blobUrl;
return (
<div className={styles.pinnedThumb}>
{thumbDataUri && !asSpoiler && !shouldRenderVideo && (
<img
className={styles.pinnedThumbImage}
src={srcUrl}
width={width}
height={height}
alt=""
draggable={false}
/>
)}
{shouldRenderVideo && !asSpoiler && (
<video
src={blobUrl}
width={width}
height={height}
playsInline
disablePictureInPicture
className={styles.pinnedThumbImage}
/>
)}
{thumbDataUri
&& <MediaSpoiler thumbDataUri={srcUrl} isVisible={Boolean(asSpoiler)} width={width} height={height} />}
</div>
);
}
if (!shouldRender || !renderingPinnedMessage) return undefined;
return (
<div
ref={ref}
className={buildClassName(
'HeaderPinnedMessageWrapper', styles.root, isFullWidth ? styles.fullWidth : styles.mini, className,
)}
>
{(pinnedMessagesCount > 1 || shouldShowLoader) && (
<Button
round
size="smaller"
color="translucent"
ariaLabel={lang('EventLogFilterPinnedMessages')}
onClick={!shouldShowLoader ? handleAllPinnedClick : undefined}
>
{isLoading && (
<Spinner
color="blue"
className={buildClassName(
styles.loading, styles.pinListIcon, !shouldShowLoader && styles.pinListIconHidden,
)}
/>
)}
<Icon
name="pin-list"
className={buildClassName(
styles.pinListIcon, shouldShowLoader && styles.pinListIconHidden,
)}
/>
</Button>
)}
{canUnpin && (
<Button
round
size="smaller"
color="translucent"
ariaLabel={lang('UnpinMessageAlertTitle')}
onClick={openUnpinDialog}
>
<Icon name="close" />
</Button>
)}
<ConfirmDialog
isOpen={isUnpinDialogOpen}
onClose={closeUnpinDialog}
text={lang('PinnedConfirmUnpin')}
confirmLabel={lang('DialogUnpin')}
confirmHandler={handleUnpinMessage}
/>
<div
className={buildClassName(styles.pinnedMessage, noHoverColor && styles.noHover)}
onClick={handleClick}
onMouseDown={handleMouseDown}
dir={lang.isRtl ? 'rtl' : undefined}
>
<PinnedMessageNavigation
count={pinnedMessagesCount}
index={currentPinnedIndex}
/>
<Transition activeKey={renderingPinnedMessage.id} name="slideVertical" className={styles.pictogramTransition}>
{renderPictogram(
mediaThumbnail,
mediaBlobUrl,
isVideoThumbnail,
isSpoiler,
)}
</Transition>
<div
className={buildClassName(styles.messageText, mediaThumbnail && styles.withMedia)}
dir={lang.isRtl ? 'rtl' : undefined}
>
<div className={styles.title} dir={lang.isRtl ? 'rtl' : undefined}>
{!topMessageTitle && (
<AnimatedCounter
text={pinnedMessagesCount === 1
? lang('PinnedMessageTitleSingle')
: lang('PinnedMessageTitle', { index: pinnedMessageNumber }, { pluralValue: pinnedMessagesCount })}
/>
)}
{topMessageTitle && renderText(topMessageTitle)}
</div>
<Transition
activeKey={renderingPinnedMessage.id}
name="slideVerticalFade"
className={styles.messageTextTransition}
>
<p dir="auto" className={styles.summary}>
<MessageSummary
message={renderingPinnedMessage}
noEmoji={Boolean(mediaThumbnail)}
emojiSize={EMOJI_SIZE}
/>
</p>
</Transition>
</div>
<RippleEffect />
{inlineButton && (
<Button
size="tiny"
className={styles.inlineButton}
onClick={handleInlineButtonClick}
shouldStopPropagation
onMouseEnter={!IS_TOUCH_ENV ? markNoHoverColor : undefined}
onMouseLeave={!IS_TOUCH_ENV ? unmarkNoHoverColor : undefined}
>
{renderKeyboardButtonText(lang, inlineButton)}
</Button>
)}
</div>
</div>
);
};
export default memo(withGlobal<OwnProps>(
(global, {
chatId, threadId, messageListType,
}): StateProps => {
const chat = selectChat(global, chatId);
const isSynced = global.isSynced;
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const messagesById = selectChatMessages(global, chatId);
const state = {
chat,
isSynced,
};
if (messageListType !== 'thread' || !messagesById) {
return state;
}
if (threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum) {
const pinnedMessageId = Number(threadId);
const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined;
const topMessageSender = message ? selectForwardedSender(global, message) : undefined;
return {
...state,
pinnedMessageIds: pinnedMessageId,
messagesById,
canUnpin: false,
topMessageSender,
};
}
const pinnedMessageIds = !isSavedDialog ? selectPinnedIds(global, chatId, threadId) : undefined;
if (pinnedMessageIds?.length) {
const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
const {
canUnpin = false,
} = (
firstPinnedMessage
&& pinnedMessageIds.length === 1
&& selectAllowedMessageActionsSlow(global, firstPinnedMessage, threadId)
) || {};
return {
...state,
pinnedMessageIds,
messagesById,
canUnpin,
};
}
return state;
},
)(HeaderPinnedMessage));

View File

@ -3,22 +3,11 @@
width: 22rem;
max-width: 100vw;
margin: 4.25rem auto 0.25rem;
margin-top: calc(4.25rem + var(--middle-header-panes-height));
z-index: var(--z-notification);
.has-header-tools & {
margin-top: 7.375rem;
}
@media (min-width: 1276px) {
transition: transform var(--layer-transition);
}
& ~ & {
margin-top: 0.25rem;
.has-header-tools & {
margin-top: 0.25rem;
}
}
}

View File

@ -154,14 +154,8 @@ export const SNAP_EFFECT_ID = 'snap-effect';
export const STARS_ICON_PLACEHOLDER = '⭐';
export const STARS_CURRENCY_CODE = 'XTR';
// Screen width where Pinned Message / Audio Player in the Middle Header can be safely displayed
export const SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN = 1440; // px
// Screen width where Pinned Message / Audio Player in the Middle Header shouldn't collapse with ChatInfo
export const SAFE_SCREEN_WIDTH_FOR_CHAT_INFO = 1150; // px
export const MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN = 1275; // px
export const MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN = 925; // px
export const MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES = 1340; // px
export const MOBILE_SCREEN_MAX_WIDTH = 600; // px
export const MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH = 950; // px
export const MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT = 450; // px

View File

@ -394,12 +394,12 @@ addActionHandler('hideAllChatJoinRequests', async (global, actions, payload): Pr
setGlobal(global);
});
addActionHandler('hideChatReportPanel', async (global, actions, payload): Promise<void> => {
addActionHandler('hideChatReportPane', async (global, actions, payload): Promise<void> => {
const { chatId } = payload!;
const chat = selectChat(global, chatId);
if (!chat) return;
const result = await callApi('hideChatReportPanel', chat);
const result = await callApi('hideChatReportPane', chat);
if (!result) return;
global = getGlobal();

View File

@ -696,10 +696,10 @@ addActionHandler('toggleMessageWebPage', (global, actions, payload): ActionRetur
addActionHandler('pinMessage', (global, actions, payload): ActionReturnType => {
const {
messageId, isUnpin, isOneSide, isSilent, tabId = getCurrentTabId(),
chatId, messageId, isUnpin, isOneSide, isSilent,
} = payload;
const chat = selectCurrentChat(global, tabId);
const chat = selectChat(global, chatId);
if (!chat) {
return;
}

View File

@ -13,6 +13,7 @@ import type { OldLangFn } from '../../hooks/useOldLang';
import type {
CustomPeer, NotifyException, NotifySettings, ThreadId,
} from '../../types';
import type { LangFn } from '../../util/localization';
import { MAIN_THREAD_ID } from '../../api/types';
import {
@ -101,7 +102,7 @@ export function getPrivateChatUserId(chat: ApiChat) {
return chat.id;
}
export function getChatTitle(lang: OldLangFn, chat: ApiChat, isSelf = false) {
export function getChatTitle(lang: OldLangFn | LangFn, chat: ApiChat, isSelf = false) {
if (isSelf) {
return lang('SavedMessages');
}

View File

@ -11,6 +11,7 @@ import type {
} from '../../api/types/messages';
import type { OldLangFn } from '../../hooks/useOldLang';
import type { ThreadId } from '../../types';
import type { LangFn } from '../../util/localization';
import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
@ -208,7 +209,7 @@ export function isAnonymousOwnMessage(message: ApiMessage) {
return Boolean(message.senderId) && !isUserId(message.senderId) && isOwnMessage(message);
}
export function getSenderTitle(lang: OldLangFn, sender: ApiPeer) {
export function getSenderTitle(lang: OldLangFn | LangFn, sender: ApiPeer) {
return isPeerUser(sender) ? getUserFullName(sender) : getChatTitle(lang, sender);
}

View File

@ -1695,11 +1695,12 @@ export interface ActionPayloads {
messageId: number;
};
pinMessage: {
chatId: string;
messageId: number;
isUnpin: boolean;
isOneSide?: boolean;
isSilent?: boolean;
} & WithTabId;
};
deleteMessages: {
messageIds: number[];
shouldDeleteForAll?: boolean;
@ -2114,7 +2115,7 @@ export interface ActionPayloads {
offsetUserId?: string;
limit?: number;
} & WithTabId;
hideChatReportPanel: {
hideChatReportPane: {
chatId: string;
};
toggleManagement: ({

View File

@ -2,6 +2,7 @@ import { useMemo } from '../lib/teact/teact';
import type {
ApiAudio, ApiChat, ApiMessage, ApiPeer, ApiVoice,
MediaContent,
} from '../api/types';
import {
@ -21,11 +22,11 @@ const MINIMAL_SIZE = 115; // spec says 100, but on Chrome 93 it's not showing
// TODO Add support for video in future
const useMessageMediaMetadata = (
message: ApiMessage, sender?: ApiPeer, chat?: ApiChat,
message?: ApiMessage, sender?: ApiPeer, chat?: ApiChat,
): MediaMetadata | undefined => {
const lang = useOldLang();
const { audio, voice } = getMessageContent(message);
const { audio, voice } = message ? getMessageContent(message) : {} satisfies MediaContent;
const title = audio ? (audio.title || audio.fileName) : voice ? 'Voice message' : '';
const artist = audio?.performer || (sender && getSenderTitle(lang, sender));
const album = (chat && getChatTitle(lang, chat)) || 'Telegram';

View File

@ -23,6 +23,7 @@ type BaseHookParams<RefType extends HTMLElement> = {
closeDuration?: number;
className?: string | false;
prefix?: string;
shouldForceOpen?: boolean;
onCloseAnimationEnd?: NoneToVoidFunction;
};
@ -66,6 +67,7 @@ export default function useShowTransition<RefType extends HTMLElement = HTMLDivE
closeDuration = CLOSE_DURATION,
className = 'fast',
prefix = '',
shouldForceOpen,
onCloseAnimationEnd,
} = params;
@ -82,6 +84,11 @@ export default function useShowTransition<RefType extends HTMLElement = HTMLDivE
useSyncEffectWithPrevDeps(([prevIsOpen]) => {
const options = optionsRef.current;
if (shouldForceOpen) {
setState('open');
return;
}
if (isOpen) {
if (closingTimeoutRef.current) {
clearTimeout(closingTimeoutRef.current);
@ -106,7 +113,7 @@ export default function useShowTransition<RefType extends HTMLElement = HTMLDivE
onCloseEndLast();
}, options.closeDuration);
}
}, [isOpen]);
}, [isOpen, shouldForceOpen]);
const applyClassNames = useLastCallback(() => {
const element = ref.current;

View File

@ -117,21 +117,19 @@
}
}
@mixin header-mobile {
@mixin header-pane {
position: absolute;
top: 100%;
left: 0;
right: 0;
top: 0;
width: 100%;
height: 2.875rem;
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
transform: translateY(-100%);
transition: transform var(--slide-transition);
display: flex;
flex-direction: row-reverse;
padding-top: 0.375rem;
padding-bottom: 0.375rem;
padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.5rem, env(safe-area-inset-right));
background: var(--color-background);
background-color: var(--color-background);
&::before {
content: "";
@ -143,6 +141,18 @@
height: 0.125rem;
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
}
// Some panels might unmount without animation, so we provide same background above panel to make it less noticeable
&::after {
content: "";
position: absolute;
top: -100%;
left: 0;
right: 0;
height: inherit;
background-color: inherit;
z-index: -1;
}
}
@mixin side-panel-section {

View File

@ -574,7 +574,9 @@ export interface LangPair {
'Statistics': undefined;
'EventLogFilterPinnedMessages': undefined;
'UnpinMessageAlertTitle': undefined;
'PinnedMessage': undefined;
'PinnedMessageTitleSingle': undefined;
'AccPinnedMessages': undefined;
'AccUnpinMessage': undefined;
'LeaveAComment': undefined;
'PollsStopWarning': undefined;
'PollsStopSure': undefined;
@ -1589,6 +1591,9 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
'PreviewSenderSendFile': {
'count': V;
};
'PinnedMessageTitle': {
'index': V;
};
'Comments': {
'count': V;
};