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; const { id, accessHash } = chat;
return invokeRequest(new GramJs.messages.HidePeerSettingsBar({ return invokeRequest(new GramJs.messages.HidePeerSettingsBar({

View File

@ -655,7 +655,11 @@
"Statistics" = "Statistics"; "Statistics" = "Statistics";
"EventLogFilterPinnedMessages" = "Pinned messages"; "EventLogFilterPinnedMessages" = "Pinned messages";
"UnpinMessageAlertTitle" = "Unpin message"; "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_one" = "{count} Comment";
"Comments_other" = "{count} Comments"; "Comments_other" = "{count} Comments";
"LeaveAComment" = "Leave a comment"; "LeaveAComment" = "Leave a comment";

View File

@ -1,37 +1,14 @@
@use "../../../styles/mixins";
.GroupCallTopPane { .GroupCallTopPane {
position: absolute; @include mixins.header-pane;
top: 100%;
left: 0;
right: 0;
height: 2.875rem;
overflow: hidden;
box-shadow: 0 2px 2px var(--color-light-shadow);
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: 0.375rem 0.5rem 0.375rem 0.75rem;
background: var(--color-background);
z-index: -1;
cursor: var(--custom-cursor, pointer); 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 { .info {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@ -56,9 +33,3 @@
width: auto; 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 useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useOldLang from '../../../hooks/useOldLang'; import useOldLang from '../../../hooks/useOldLang';
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated'; import useHeaderPane, { type PaneState } from '../../middle/hooks/useHeaderPane';
import AvatarList from '../../common/AvatarList'; import AvatarList from '../../common/AvatarList';
import Button from '../../ui/Button'; import Button from '../../ui/Button';
@ -21,8 +21,8 @@ import './GroupCallTopPane.scss';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
hasPinnedOffset: boolean;
className?: string; className?: string;
onPaneStateChange?: (state: PaneState) => void;
}; };
type StateProps = { type StateProps = {
@ -37,7 +37,7 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
isActive, isActive,
className, className,
groupCall, groupCall,
hasPinnedOffset, onPaneStateChange,
}) => { }) => {
const { const {
requestMasterAndJoinGroupCall, requestMasterAndJoinGroupCall,
@ -86,23 +86,24 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
}; };
}, [groupCall?.id, groupCall?.isLoaded, isActive, subscribeToGroupCallUpdates]); }, [groupCall?.id, groupCall?.isLoaded, isActive, subscribeToGroupCallUpdates]);
const {
shouldRender,
transitionClassNames,
} = useShowTransitionDeprecated(Boolean(groupCall && isActive));
const renderingParticipantCount = useCurrentOrPrev(groupCall?.participantsCount, true); const renderingParticipantCount = useCurrentOrPrev(groupCall?.participantsCount, true);
const renderingFetchedParticipants = useCurrentOrPrev(fetchedParticipants, true); const renderingFetchedParticipants = useCurrentOrPrev(fetchedParticipants, true);
const isRendering = Boolean(groupCall && isActive);
const { ref, shouldRender } = useHeaderPane({
isOpen: isRendering,
onStateChange: onPaneStateChange,
});
if (!shouldRender) return undefined; if (!shouldRender) return undefined;
return ( return (
<div <div
ref={ref}
className={buildClassName( className={buildClassName(
'GroupCallTopPane', 'GroupCallTopPane',
hasPinnedOffset && 'has-pinned-offset',
className, className,
transitionClassNames,
)} )}
onClick={handleJoinGroupCall} onClick={handleJoinGroupCall}
> >

View File

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

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import React, {
import { addExtraClass } from '../../lib/teact/teact-dom'; import { addExtraClass } from '../../lib/teact/teact-dom';
import { getActions, getGlobal, withGlobal } from '../../global'; 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 type { ApiLimitTypeWithModal, TabState } from '../../global/types';
import { ElectronEvent } from '../../types/electron'; import { ElectronEvent } from '../../types/electron';
@ -61,10 +61,10 @@ import StickerSetModal from '../common/StickerSetModal.async';
import UnreadCount from '../common/UnreadCounter'; import UnreadCount from '../common/UnreadCounter';
import LeftColumn from '../left/LeftColumn'; import LeftColumn from '../left/LeftColumn';
import MediaViewer from '../mediaViewer/MediaViewer.async'; import MediaViewer from '../mediaViewer/MediaViewer.async';
import AudioPlayer from '../middle/AudioPlayer';
import ReactionPicker from '../middle/message/reactions/ReactionPicker.async'; import ReactionPicker from '../middle/message/reactions/ReactionPicker.async';
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler'; import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
import MiddleColumn from '../middle/MiddleColumn'; import MiddleColumn from '../middle/MiddleColumn';
import AudioPlayer from '../middle/panes/AudioPlayer';
import ModalContainer from '../modals/ModalContainer'; import ModalContainer from '../modals/ModalContainer';
import PaymentModal from '../payment/PaymentModal.async'; import PaymentModal from '../payment/PaymentModal.async';
import ReceiptModal from '../payment/ReceiptModal.async'; import ReceiptModal from '../payment/ReceiptModal.async';
@ -107,7 +107,6 @@ type StateProps = {
isForwardModalOpen: boolean; isForwardModalOpen: boolean;
hasNotifications: boolean; hasNotifications: boolean;
hasDialogs: boolean; hasDialogs: boolean;
audioMessage?: ApiMessage;
safeLinkModalUrl?: string; safeLinkModalUrl?: string;
isHistoryCalendarOpen: boolean; isHistoryCalendarOpen: boolean;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
@ -159,7 +158,6 @@ const Main = ({
isForwardModalOpen, isForwardModalOpen,
hasNotifications, hasNotifications,
hasDialogs, hasDialogs,
audioMessage,
activeGroupCallId, activeGroupCallId,
safeLinkModalUrl, safeLinkModalUrl,
isHistoryCalendarOpen, isHistoryCalendarOpen,
@ -545,7 +543,7 @@ const Main = ({
<DraftRecipientPicker requestedDraft={requestedDraft} /> <DraftRecipientPicker requestedDraft={requestedDraft} />
<Notifications isOpen={hasNotifications} /> <Notifications isOpen={hasNotifications} />
<Dialogs isOpen={hasDialogs} /> <Dialogs isOpen={hasDialogs} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />} <AudioPlayer noUi />
<ModalContainer /> <ModalContainer />
<SafeLinkModal url={safeLinkModalUrl} /> <SafeLinkModal url={safeLinkModalUrl} />
<HistoryCalendar isOpen={isHistoryCalendarOpen} /> <HistoryCalendar isOpen={isHistoryCalendarOpen} />
@ -613,7 +611,6 @@ export default memo(withGlobal<OwnProps>(
openedCustomEmojiSetIds, openedCustomEmojiSetIds,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
openedGame, openedGame,
audioPlayer,
isLeftColumnShown, isLeftColumnShown,
historyCalendarSelectedAt, historyCalendarSelectedAt,
notifications, notifications,
@ -630,10 +627,6 @@ export default memo(withGlobal<OwnProps>(
deleteFolderDialogModal, deleteFolderDialogModal,
} = selectTabState(global); } = 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 gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
const gameTitle = gameMessage?.content.game?.title; const gameTitle = gameMessage?.content.game?.title;
const { chatId } = selectCurrentMessageList(global) || {}; const { chatId } = selectCurrentMessageList(global) || {};
@ -653,7 +646,6 @@ export default memo(withGlobal<OwnProps>(
isReactionPickerOpen: selectIsReactionPickerOpen(global), isReactionPickerOpen: selectIsReactionPickerOpen(global),
hasNotifications: Boolean(notifications.length), hasNotifications: Boolean(notifications.length),
hasDialogs: Boolean(dialogs.length), hasDialogs: Boolean(dialogs.length),
audioMessage,
safeLinkModalUrl, safeLinkModalUrl,
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt), isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
shouldSkipHistoryAnimations, 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 { &.no-composer {
margin-bottom: 0; margin-bottom: 0;
@ -379,17 +383,13 @@
&.scrolled:not(.is-animating) .sticky-date { &.scrolled:not(.is-animating) .sticky-date {
position: sticky; position: sticky;
top: 0.625rem; top: calc(0.625rem + var(--middle-header-panes-height));
} }
&.is-animating .message-select-control { &.is-animating .message-select-control {
display: none !important; display: none !important;
} }
.has-header-tools & .sticky-date {
top: 3.75rem !important;
}
.local-action-message, .local-action-message,
.ActionMessage { .ActionMessage {
margin-top: 0.5rem; margin-top: 0.5rem;

View File

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

View File

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

View File

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

View File

@ -1,33 +1,9 @@
@use "../../styles/mixins"; @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 { .MiddleHeader {
display: flex; display: flex;
align-items: center; align-items: center;
width: 100%; width: 100%;
box-shadow: 0 2px 2px var(--color-light-shadow);
background: var(--color-background); background: var(--color-background);
position: relative; position: relative;
z-index: var(--z-middle-header); z-index: var(--z-middle-header);
@ -118,18 +94,7 @@
} }
} }
@media (min-width: 1276px) and (max-width: 1439px) { @media (min-width: 1276px) {
.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) {
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
transition: transform var(--layer-transition); 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 { h3 {
font-weight: 500; font-weight: 500;
font-size: 1.125rem; font-size: 1.125rem;
@ -384,10 +321,6 @@
} }
} }
@media (max-width: 600px) {
@include mobile-header-styles();
}
body.is-electron.is-macos & { body.is-electron.is-macos & {
-webkit-app-region: drag; -webkit-app-region: drag;
} }

View File

@ -1,11 +1,11 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../lib/teact/teact';
import React, { import React, {
memo, useEffect, useLayoutEffect, useRef, memo, useRef,
} from '../../lib/teact/teact'; } from '../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../global';
import type { import type {
ApiChat, ApiMessage, ApiPeer, ApiSticker, ApiTypingStatus, ApiChat, ApiMessage, ApiSticker, ApiTypingStatus,
} from '../../api/types'; } from '../../api/types';
import type { GlobalState, MessageListType } from '../../global/types'; import type { GlobalState, MessageListType } from '../../global/types';
import type { Signal } from '../../util/signals'; import type { Signal } from '../../util/signals';
@ -14,35 +14,18 @@ import { StoryViewerOrigin, type ThreadId } from '../../types';
import { import {
EDITABLE_INPUT_CSS_SELECTOR, EDITABLE_INPUT_CSS_SELECTOR,
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN, 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'; } from '../../config';
import { requestMutation } from '../../lib/fasterdom/fasterdom';
import { import {
getChatTitle,
getIsSavedDialog, getIsSavedDialog,
getSenderTitle,
isChatChannel,
isChatSuperGroup,
isUserId, isUserId,
} from '../../global/helpers'; } from '../../global/helpers';
import { import {
selectAllowedMessageActionsSlow,
selectChat, selectChat,
selectChatMessage, selectChatMessage,
selectChatMessages,
selectCurrentMiddleSearch,
selectForwardedSender,
selectIsChatBotNotStarted,
selectIsChatWithBot,
selectIsChatWithSelf, selectIsChatWithSelf,
selectIsInSelectMode, selectIsInSelectMode,
selectIsRightColumnShown, selectIsRightColumnShown,
selectIsUserBlocked,
selectPinnedIds, selectPinnedIds,
selectScheduledIds, selectScheduledIds,
selectTabState, selectTabState,
@ -50,36 +33,27 @@ import {
selectThreadParam, selectThreadParam,
} from '../../global/selectors'; } from '../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../util/buildClassName';
import cycleRestrict from '../../util/cycleRestrict';
import { getMessageKey } from '../../util/keys/messageKey';
import useAppLayout from '../../hooks/useAppLayout'; import useAppLayout from '../../hooks/useAppLayout';
import useConnectionStatus from '../../hooks/useConnectionStatus'; import useConnectionStatus from '../../hooks/useConnectionStatus';
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
import useDerivedState from '../../hooks/useDerivedState';
import useElectronDrag from '../../hooks/useElectronDrag'; import useElectronDrag from '../../hooks/useElectronDrag';
import useEnsureMessage from '../../hooks/useEnsureMessage';
import useLastCallback from '../../hooks/useLastCallback'; import useLastCallback from '../../hooks/useLastCallback';
import useLongPress from '../../hooks/useLongPress'; import useLongPress from '../../hooks/useLongPress';
import useOldLang from '../../hooks/useOldLang'; import useOldLang from '../../hooks/useOldLang';
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated'; import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
import useWindowSize from '../../hooks/window/useWindowSize'; import useWindowSize from '../../hooks/window/useWindowSize';
import GroupCallTopPane from '../calls/group/GroupCallTopPane';
import GroupChatInfo from '../common/GroupChatInfo'; import GroupChatInfo from '../common/GroupChatInfo';
import PrivateChatInfo from '../common/PrivateChatInfo'; import PrivateChatInfo from '../common/PrivateChatInfo';
import UnreadCounter from '../common/UnreadCounter'; import UnreadCounter from '../common/UnreadCounter';
import Button from '../ui/Button'; import Button from '../ui/Button';
import Transition from '../ui/Transition'; import Transition from '../ui/Transition';
import AudioPlayer from './AudioPlayer';
import ChatReportPanel from './ChatReportPanel';
import HeaderActions from './HeaderActions'; import HeaderActions from './HeaderActions';
import HeaderPinnedMessage from './HeaderPinnedMessage'; import AudioPlayer from './panes/AudioPlayer';
import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
import './MiddleHeader.scss'; import './MiddleHeader.scss';
const ANIMATION_DURATION = 350;
const BACK_BUTTON_INACTIVE_TIME = 450; const BACK_BUTTON_INACTIVE_TIME = 450;
const EMOJI_STATUS_SIZE = 22; const EMOJI_STATUS_SIZE = 22;
const SEARCH_LONGTAP_THRESHOLD = 500; const SEARCH_LONGTAP_THRESHOLD = 500;
@ -89,7 +63,6 @@ type OwnProps = {
threadId: ThreadId; threadId: ThreadId;
messageListType: MessageListType; messageListType: MessageListType;
isComments?: boolean; isComments?: boolean;
isReady?: boolean;
isMobile?: boolean; isMobile?: boolean;
getCurrentPinnedIndex: Signal<number>; getCurrentPinnedIndex: Signal<number>;
getLoadingPinnedId: Signal<number | undefined>; getLoadingPinnedId: Signal<number | undefined>;
@ -98,11 +71,7 @@ type OwnProps = {
type StateProps = { type StateProps = {
chat?: ApiChat; chat?: ApiChat;
pinnedMessageIds?: number[] | number;
messagesById?: Record<number, ApiMessage>;
canUnpin?: boolean;
isSavedDialog?: boolean; isSavedDialog?: boolean;
topMessageSender?: ApiPeer;
typingStatus?: ApiTypingStatus; typingStatus?: ApiTypingStatus;
isSelectModeActive?: boolean; isSelectModeActive?: boolean;
isLeftColumnShown?: boolean; isLeftColumnShown?: boolean;
@ -110,61 +79,45 @@ type StateProps = {
audioMessage?: ApiMessage; audioMessage?: ApiMessage;
messagesCount?: number; messagesCount?: number;
isChatWithSelf?: boolean; isChatWithSelf?: boolean;
hasButtonInHeader?: boolean;
shouldSkipHistoryAnimations?: boolean; shouldSkipHistoryAnimations?: boolean;
currentTransitionKey: number; currentTransitionKey: number;
connectionState?: GlobalState['connectionState']; connectionState?: GlobalState['connectionState'];
isSyncing?: boolean; isSyncing?: boolean;
isSynced?: boolean;
isFetchingDifference?: boolean; isFetchingDifference?: boolean;
emojiStatusSticker?: ApiSticker; emojiStatusSticker?: ApiSticker;
isMiddleSearchOpen?: boolean;
}; };
const MiddleHeader: FC<OwnProps & StateProps> = ({ const MiddleHeader: FC<OwnProps & StateProps> = ({
chatId, chatId,
threadId, threadId,
messageListType, messageListType,
isReady,
isMobile, isMobile,
pinnedMessageIds,
messagesById,
canUnpin,
topMessageSender,
typingStatus, typingStatus,
isSelectModeActive, isSelectModeActive,
isLeftColumnShown, isLeftColumnShown,
isRightColumnShown,
audioMessage, audioMessage,
chat, chat,
messagesCount, messagesCount,
isComments, isComments,
isChatWithSelf, isChatWithSelf,
hasButtonInHeader,
shouldSkipHistoryAnimations, shouldSkipHistoryAnimations,
currentTransitionKey, currentTransitionKey,
connectionState, connectionState,
isSyncing, isSyncing,
isSynced,
isFetchingDifference, isFetchingDifference,
getCurrentPinnedIndex, getCurrentPinnedIndex,
getLoadingPinnedId, getLoadingPinnedId,
emojiStatusSticker, emojiStatusSticker,
isSavedDialog, isSavedDialog,
isMiddleSearchOpen,
onFocusPinnedMessage, onFocusPinnedMessage,
}) => { }) => {
const { const {
openThreadWithInfo, openThreadWithInfo,
pinMessage,
focusMessage,
openChat, openChat,
openPreviousChat, openPreviousChat,
loadPinnedMessages,
toggleLeftColumn, toggleLeftColumn,
exitMessageSelectMode, exitMessageSelectMode,
openPremiumModal, openPremiumModal,
openThread,
openStickerSet, openStickerSet,
updateMiddleSearch, updateMiddleSearch,
} = getActions(); } = getActions();
@ -173,32 +126,15 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
const isBackButtonActive = useRef(true); const isBackButtonActive = useRef(true);
const { isTablet } = useAppLayout(); 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 { width: windowWidth } = useWindowSize();
const { isDesktop } = useAppLayout();
const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN; const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN;
const shouldShowCloseButton = isTablet && isLeftColumnShown; const shouldShowCloseButton = isTablet && isLeftColumnShown;
// eslint-disable-next-line no-null/no-null // eslint-disable-next-line no-null/no-null
const componentRef = useRef<HTMLDivElement>(null); const componentRef = useRef<HTMLDivElement>(null);
const shouldAnimateTools = useRef<boolean>(true);
const handleOpenSearch = useLastCallback(() => { const handleOpenSearch = useLastCallback(() => {
updateMiddleSearch({ chatId, threadId, update: {} }); updateMiddleSearch({ chatId, threadId, update: {} });
@ -222,27 +158,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
threshold: SEARCH_LONGTAP_THRESHOLD, 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(() => { const setBackButtonActive = useLastCallback(() => {
setTimeout(() => { setTimeout(() => {
isBackButtonActive.current = true; isBackButtonActive.current = true;
@ -292,81 +207,14 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
setBackButtonActive(); 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 prevTransitionKey = usePreviousDeprecated(currentTransitionKey);
const cleanupExceptionKey = ( const cleanupExceptionKey = (
prevTransitionKey !== undefined && prevTransitionKey < currentTransitionKey ? prevTransitionKey : undefined prevTransitionKey !== undefined && prevTransitionKey < currentTransitionKey ? prevTransitionKey : undefined
); );
const canRevealTools = (shouldRenderPinnedMessage && renderingPinnedMessage) const isAudioPlayerActive = Boolean(audioMessage);
|| (shouldRenderAudioPlayer && renderingAudioMessage); const isAudioPlayerRendering = isDesktop && isAudioPlayerActive;
const isPinnedMessagesFullWidth = isAudioPlayerActive || !isDesktop;
// 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 { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing || isFetchingDifference, true); 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); useElectronDrag(componentRef);
return ( return (
@ -486,56 +330,28 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
> >
{renderInfo()} {renderInfo()}
</Transition> </Transition>
{!isPinnedMessagesFullWidth && (
{threadId === MAIN_THREAD_ID && !chat?.isForum && (
<GroupCallTopPane
hasPinnedOffset={
(shouldRenderPinnedMessage && Boolean(renderingPinnedMessage))
|| (shouldRenderAudioPlayer && Boolean(renderingAudioMessage))
}
chatId={chatId}
/>
)}
{shouldRenderPinnedMessage && renderingPinnedMessage && (
<HeaderPinnedMessage <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} key={chatId}
chatId={chatId} chatId={chatId}
settings={renderingChatSettings} threadId={threadId}
className={chatReportPanelClassNames} messageListType={messageListType}
onFocusPinnedMessage={onFocusPinnedMessage}
getLoadingPinnedId={getLoadingPinnedId}
getCurrentPinnedIndex={getCurrentPinnedIndex}
/> />
)} )}
<div className="header-tools"> <div className="header-tools">
{isAudioPlayerRendered && ( {isAudioPlayerRendering && (
<AudioPlayer <AudioPlayer />
key={getMessageKey(renderingAudioMessage!)}
message={renderingAudioMessage!}
className={audioPlayerClassNames}
/>
)} )}
<HeaderActions <HeaderActions
chatId={chatId} chatId={chatId}
threadId={threadId} threadId={threadId}
messageListType={messageListType} messageListType={messageListType}
isMobile={isMobile} isMobile={isMobile}
canExpandActions={!isAudioPlayerRendered} canExpandActions={!isAudioPlayerRendering}
/> />
</div> </div>
</div> </div>
@ -568,23 +384,14 @@ export default memo(withGlobal<OwnProps>(
messagesCount = threadInfo?.messagesCount || 0; 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 typingStatus = selectThreadParam(global, chatId, threadId, 'typingStatus');
const emojiStatus = chat?.emojiStatus; const emojiStatus = chat?.emojiStatus;
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId]; const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId); const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
const isMiddleSearchOpen = Boolean(selectCurrentMiddleSearch(global));
const state: StateProps = { return {
typingStatus, typingStatus,
isLeftColumnShown, isLeftColumnShown,
isRightColumnShown: selectIsRightColumnShown(global, isMobile), isRightColumnShown: selectIsRightColumnShown(global, isMobile),
@ -597,52 +404,9 @@ export default memo(withGlobal<OwnProps>(
currentTransitionKey: Math.max(0, messageLists.length - 1), currentTransitionKey: Math.max(0, messageLists.length - 1),
connectionState: global.connectionState, connectionState: global.connectionState,
isSyncing: global.isSyncing, isSyncing: global.isSyncing,
isSynced: global.isSynced,
isFetchingDifference: global.isFetchingDifference, isFetchingDifference: global.isFetchingDifference,
emojiStatusSticker, emojiStatusSticker,
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
isSavedDialog, 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)); )(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 React, { type TeactNode } from '../../../../lib/teact/teact';
import type { ApiKeyboardButton } from '../../../../api/types'; import type { ApiKeyboardButton } from '../../../../api/types';
import type { LangFn } from '../../../../util/localization';
import { STARS_ICON_PLACEHOLDER } from '../../../../config'; import { STARS_ICON_PLACEHOLDER } from '../../../../config';
import { replaceWithTeact } from '../../../../util/replaceWithTeact'; import { replaceWithTeact } from '../../../../util/replaceWithTeact';
@ -10,7 +11,7 @@ import { type OldLangFn } from '../../../../hooks/useOldLang';
import Icon from '../../../common/icons/Icon'; 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') { if (button.type === 'receipt') {
return lang('PaymentReceipt'); 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 DEBOUNCE = 1000;
const STICKY_TOP = 10; const STICKY_TOP = 10;
const STICKY_TOP_WITH_TOOLS = 60;
export default function useStickyDates() { export default function useStickyDates() {
// For some reason we can not synchronously hide a sticky element (from `useLayoutEffect`) when chat opens // 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 runDebounced = useRunDebounced(DEBOUNCE, true);
const updateStickyDates = useLastCallback((container: HTMLDivElement, hasTools?: boolean) => { const updateStickyDates = useLastCallback((container: HTMLDivElement) => {
markIsScrolled(); markIsScrolled();
if (!document.body.classList.contains('is-scrolling-messages')) { if (!document.body.classList.contains('is-scrolling-messages')) {
@ -25,7 +24,7 @@ export default function useStickyDates() {
} }
runDebounced(() => { runDebounced(() => {
const stuckDateEl = findStuckDate(container, hasTools); const stuckDateEl = findStuckDate(container);
if (stuckDateEl) { if (stuckDateEl) {
requestMutation(() => { requestMutation(() => {
stuckDateEl.classList.add('stuck'); 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 allElements = container.querySelectorAll<HTMLDivElement>('.sticky-date');
const containerTop = container.scrollTop; const containerTop = container.scrollTop;
const computedStyle = getComputedStyle(container);
const headerActionsHeight = parseInt(computedStyle.getPropertyValue('--middle-header-panes-height'), 10);
return Array.from(allElements).find((el) => { return Array.from(allElements).find((el) => {
const { offsetTop, offsetHeight } = el; const { offsetTop, offsetHeight } = el;
const top = offsetTop - containerTop; 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(() => { const handleUnpin = useLastCallback(() => {
pinMessage({ messageId: message.id, isUnpin: true }); pinMessage({ chatId: message.chatId, messageId: message.id, isUnpin: true });
closeMenu(); closeMenu();
}); });

View File

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

View File

@ -1,8 +1,9 @@
@use "../../../styles/mixins";
.AudioPlayer { .AudioPlayer {
display: flex; display: flex;
align-items: center;
margin-left: auto; margin-left: auto;
margin-top: -0.25rem;
margin-bottom: -0.25rem;
body.no-page-transitions & { body.no-page-transitions & {
transition: none !important; transition: none !important;
@ -170,10 +171,6 @@
max-width: 10rem; max-width: 10rem;
} }
@media (max-width: 600px) {
max-width: 100%;
}
@media (min-width: 1440px) { @media (min-width: 1440px) {
max-width: 24rem; max-width: 24rem;
.right-column-shown & { .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 { .playback-rate-menu .bubble {
min-width: auto; min-width: auto;

View File

@ -1,47 +1,54 @@
import type { FC } from '../../lib/teact/teact'; import type { FC } from '../../../lib/teact/teact';
import React, { useMemo, useRef } from '../../lib/teact/teact'; import React, { useMemo } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../../global';
import type { import type {
ApiAudio, ApiChat, ApiMessage, ApiPeer, ApiAudio, ApiChat, ApiMessage, ApiPeer,
} from '../../api/types'; MediaContent,
import type { AudioOrigin } from '../../types'; } from '../../../api/types';
import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../config'; import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../../config';
import { import {
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal, getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal,
} from '../../global/helpers'; } from '../../../global/helpers';
import { selectChat, selectSender, selectTabState } from '../../global/selectors'; import {
import { makeTrackId } from '../../util/audioPlayer'; selectChat, selectChatMessage, selectSender, selectTabState,
import buildClassName from '../../util/buildClassName'; } from '../../../global/selectors';
import * as mediaLoader from '../../util/mediaLoader'; import { makeTrackId } from '../../../util/audioPlayer';
import { clearMediaSession } from '../../util/mediaSession'; import buildClassName from '../../../util/buildClassName';
import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment'; import * as mediaLoader from '../../../util/mediaLoader';
import renderText from '../common/helpers/renderText'; 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 useAppLayout from '../../../hooks/useAppLayout';
import useAudioPlayer from '../../hooks/useAudioPlayer'; import useAudioPlayer from '../../../hooks/useAudioPlayer';
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers'; import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
import useLastCallback from '../../hooks/useLastCallback'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useMessageMediaMetadata from '../../hooks/useMessageMediaMetadata'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../hooks/useOldLang'; 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 Button from '../../ui/Button';
import DropdownMenu from '../ui/DropdownMenu'; import DropdownMenu from '../../ui/DropdownMenu';
import MenuItem from '../ui/MenuItem'; import MenuItem from '../../ui/MenuItem';
import RangeSlider from '../ui/RangeSlider'; import RangeSlider from '../../ui/RangeSlider';
import RippleEffect from '../ui/RippleEffect'; import RippleEffect from '../../ui/RippleEffect';
import './AudioPlayer.scss'; import './AudioPlayer.scss';
type OwnProps = { type OwnProps = {
message: ApiMessage;
origin?: AudioOrigin;
className?: string; className?: string;
noUi?: boolean; noUi?: boolean;
isFullWidth?: boolean;
isHidden?: boolean;
onPaneStateChange?: (state: PaneState) => void;
}; };
type StateProps = { type StateProps = {
message?: ApiMessage;
sender?: ApiPeer; sender?: ApiPeer;
chat?: ApiChat; chat?: ApiChat;
volume: number; volume: number;
@ -72,6 +79,8 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
playbackRate, playbackRate,
isPlaybackRateActive, isPlaybackRateActive,
isMuted, isMuted,
isFullWidth,
onPaneStateChange,
}) => { }) => {
const { const {
setAudioPlayerVolume, setAudioPlayerVolume,
@ -81,16 +90,19 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
closeAudioPlayer, closeAudioPlayer,
} = getActions(); } = getActions();
// eslint-disable-next-line no-null/no-null
const ref = useRef<HTMLDivElement>(null);
const lang = useOldLang(); const lang = useOldLang();
const { isMobile } = useAppLayout(); 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 isVoice = Boolean(voice || video);
const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION; const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION;
const senderName = sender ? getSenderTitle(lang, sender) : undefined; 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 { const {
playPause, playPause,
@ -104,8 +116,8 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
toggleMuted, toggleMuted,
setPlaybackRate, setPlaybackRate,
} = useAudioPlayer( } = useAudioPlayer(
makeTrackId(message), message && makeTrackId(message),
getMediaDuration(message)!, message ? getMediaDuration(message)! : 0,
isVoice ? 'voice' : 'audio', isVoice ? 'voice' : 'audio',
mediaData, mediaData,
undefined, undefined,
@ -114,18 +126,34 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
true, true,
undefined, undefined,
undefined, undefined,
isMessageLocal(message), message && isMessageLocal(message),
true, 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 { const {
isContextMenuOpen, isContextMenuOpen,
handleBeforeContextMenu, handleContextMenu, handleBeforeContextMenu, handleContextMenu,
handleContextMenuClose, handleContextMenuHide, handleContextMenuClose, handleContextMenuHide,
} = useContextMenuHandlers(ref); } = useContextMenuHandlers(transitionRef, !shouldRender);
const handleClick = useLastCallback(() => { const handleClick = useLastCallback(() => {
focusMessage({ chatId: message.chatId, messageId: message.id }); const { chatId, id } = renderingMessage!;
focusMessage({ chatId, messageId: id });
}); });
const handleClose = useLastCallback(() => { const handleClose = useLastCallback(() => {
@ -221,12 +249,16 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
return 'icon-volume-3'; return 'icon-volume-3';
}, [volume, isMuted]); }, [volume, isMuted]);
if (noUi) { if (noUi || !shouldRender) {
return undefined; return undefined;
} }
return ( 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}> <div className="AudioPlayer-content" onClick={handleClick}>
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)} {audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
<RippleEffect /> <RippleEffect />
@ -365,14 +397,19 @@ function renderPlaybackRateMenuItem(
} }
export default withGlobal<OwnProps>( export default withGlobal<OwnProps>(
(global, { message }): StateProps => { (global, { isHidden }): StateProps => {
const sender = selectSender(global, message); const { audioPlayer } = selectTabState(global);
const chat = selectChat(global, message.chatId); 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 { const {
volume, playbackRate, isMuted, isPlaybackRateActive, volume, playbackRate, isMuted, isPlaybackRateActive,
} = selectTabState(global).audioPlayer; } = selectTabState(global).audioPlayer;
return { return {
message,
sender, sender,
chat, chat,
volume, 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 type { FC } from '../../../lib/teact/teact';
import React, { memo, useState } from '../../lib/teact/teact'; import React, { memo, useState } from '../../../lib/teact/teact';
import { getActions, withGlobal } from '../../global'; import { getActions, withGlobal } from '../../../global';
import type { ApiChat, ApiChatSettings, ApiUser } from '../../api/types'; import type { ApiChat, ApiUser } from '../../../api/types';
import { import {
getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup, getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup,
} from '../../global/helpers'; } from '../../../global/helpers';
import { selectChat, selectUser } from '../../global/selectors'; import { selectChat, selectUser } from '../../../global/selectors';
import buildClassName from '../../util/buildClassName'; import buildClassName from '../../../util/buildClassName';
import useFlag from '../../hooks/useFlag'; import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
import useLastCallback from '../../hooks/useLastCallback'; import useFlag from '../../../hooks/useFlag';
import useOldLang from '../../hooks/useOldLang'; import useLastCallback from '../../../hooks/useLastCallback';
import useOldLang from '../../../hooks/useOldLang';
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
import Button from '../ui/Button'; import Icon from '../../common/icons/Icon';
import Checkbox from '../ui/Checkbox'; import Button from '../../ui/Button';
import ConfirmDialog from '../ui/ConfirmDialog'; import Checkbox from '../../ui/Checkbox';
import ConfirmDialog from '../../ui/ConfirmDialog';
import './ChatReportPanel.scss'; import './ChatReportPane.scss';
type OwnProps = { type OwnProps = {
chatId: string; chatId: string;
className?: string; className?: string;
settings?: ApiChatSettings; isAutoArchived?: boolean;
canReportSpam?: boolean;
canAddContact?: boolean;
canBlockContact?: boolean;
onPaneStateChange?: (state: PaneState) => void;
}; };
type StateProps = { type StateProps = {
@ -32,8 +39,17 @@ type StateProps = {
user?: ApiUser; user?: ApiUser;
}; };
const ChatReportPanel: FC<OwnProps & StateProps> = ({ const ChatReportPane: FC<OwnProps & StateProps> = ({
chatId, className, chat, user, settings, currentUserId, chatId,
className,
isAutoArchived,
canReportSpam,
canAddContact,
canBlockContact,
chat,
user,
currentUserId,
onPaneStateChange,
}) => { }) => {
const { const {
openAddContactDialog, openAddContactDialog,
@ -44,21 +60,23 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
deleteChatUser, deleteChatUser,
deleteHistory, deleteHistory,
toggleChatArchived, toggleChatArchived,
hideChatReportPanel, hideChatReportPane,
} = getActions(); } = getActions();
const lang = useOldLang(); const lang = useOldLang();
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag(); const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
const [shouldReportSpam, setShouldReportSpam] = useState<boolean>(true); const [shouldReportSpam, setShouldReportSpam] = useState<boolean>(true);
const [shouldDeleteChat, setShouldDeleteChat] = useState<boolean>(true); const [shouldDeleteChat, setShouldDeleteChat] = useState<boolean>(true);
const {
isAutoArchived, canReportSpam, canAddContact, canBlockContact,
} = settings || {};
const isBasicGroup = chat && isChatBasicGroup(chat); const isBasicGroup = chat && isChatBasicGroup(chat);
const renderingCanAddContact = useCurrentOrPrev(canAddContact);
const renderingCanBlockContact = useCurrentOrPrev(canBlockContact);
const renderingCanReportSpam = useCurrentOrPrev(canReportSpam);
const renderingIsAutoArchived = useCurrentOrPrev(isAutoArchived);
const handleAddContact = useLastCallback(() => { const handleAddContact = useLastCallback(() => {
openAddContactDialog({ userId: chatId }); openAddContactDialog({ userId: chatId });
if (isAutoArchived) { if (renderingIsAutoArchived) {
toggleChatArchived({ id: chatId }); toggleChatArchived({ id: chatId });
} }
}); });
@ -66,7 +84,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
const handleConfirmBlock = useLastCallback(() => { const handleConfirmBlock = useLastCallback(() => {
closeBlockUserModal(); closeBlockUserModal();
blockUser({ userId: chatId }); blockUser({ userId: chatId });
if (canReportSpam && shouldReportSpam) { if (renderingCanReportSpam && shouldReportSpam) {
reportSpam({ chatId }); reportSpam({ chatId });
} }
if (shouldDeleteChat) { if (shouldDeleteChat) {
@ -74,8 +92,8 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
} }
}); });
const handleCloseReportPanel = useLastCallback(() => { const handleCloseReportPane = useLastCallback(() => {
hideChatReportPanel({ chatId }); hideChatReportPane({ chatId });
}); });
const handleChatReportSpam = useLastCallback(() => { const handleChatReportSpam = useLastCallback(() => {
@ -89,42 +107,53 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
} }
}); });
if (!settings || (!chat && !user)) { const hasAnyButton = canAddContact || canBlockContact || canReportSpam;
return undefined;
} const isRendering = Boolean(hasAnyButton && (chat || user));
const { ref, shouldRender } = useHeaderPane({
isOpen: isRendering,
onStateChange: onPaneStateChange,
});
if (!shouldRender) return undefined;
return ( return (
<div className={buildClassName('ChatReportPanel', className)} dir={lang.isRtl ? 'rtl' : undefined}> <div
{canAddContact && ( ref={ref}
className={buildClassName('ChatReportPane', className)}
dir={lang.isRtl ? 'rtl' : undefined}
>
{renderingCanAddContact && (
<Button <Button
isText isText
fluid fluid
size="tiny" size="tiny"
className="UserReportPanel--Button" className="ChatReportPane--Button"
onClick={handleAddContact} onClick={handleAddContact}
> >
{lang('lng_new_contact_add')} {lang('lng_new_contact_add')}
</Button> </Button>
)} )}
{canBlockContact && ( {renderingCanBlockContact && (
<Button <Button
color="danger" color="danger"
isText isText
fluid fluid
size="tiny" size="tiny"
className="UserReportPanel--Button" className="ChatReportPane--Button"
onClick={openBlockUserModal} onClick={openBlockUserModal}
> >
{lang('lng_new_contact_block')} {lang('lng_new_contact_block')}
</Button> </Button>
)} )}
{canReportSpam && !canBlockContact && ( {renderingCanReportSpam && !renderingCanBlockContact && (
<Button <Button
color="danger" color="danger"
isText isText
fluid fluid
size="tiny" size="tiny"
className="UserReportPanel--Button" className="ChatReportPane--Button"
onClick={openBlockUserModal} onClick={openBlockUserModal}
> >
{lang('lng_report_spam_and_leave')} {lang('lng_report_spam_and_leave')}
@ -133,12 +162,12 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
<Button <Button
round round
ripple ripple
size="tiny" size="smaller"
color="translucent" color="translucent"
onClick={handleCloseReportPanel} onClick={handleCloseReportPane}
ariaLabel={lang('Close')} ariaLabel={lang('Close')}
> >
<i className="icon icon-close" /> <Icon name="close" />
</Button> </Button>
<ConfirmDialog <ConfirmDialog
isOpen={isBlockUserModalOpen} isOpen={isBlockUserModalOpen}
@ -176,4 +205,4 @@ export default memo(withGlobal<OwnProps>(
chat: selectChat(global, chatId), chat: selectChat(global, chatId),
user: selectUser(global, chatId), user: selectUser(global, chatId),
}), }),
)(ChatReportPanel)); )(ChatReportPane));

View File

@ -1,4 +1,4 @@
@use "../../styles/mixins"; @use "../../../styles/mixins";
.root { .root {
display: flex; display: flex;
@ -26,6 +26,12 @@
transition: none !important; transition: none !important;
} }
> :global(.Button) {
flex-shrink: 0;
}
}
.mini {
@media (min-width: 1276px) { @media (min-width: 1276px) {
transform: translate3d(0, 0, 0); transform: translate3d(0, 0, 0);
transition: opacity 0.15s ease, transform var(--layer-transition); transition: opacity 0.15s ease, transform var(--layer-transition);
@ -38,33 +44,10 @@
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0); transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
} }
} }
> :global(.Button) {
flex-shrink: 0;
}
} }
.root:global(.full-width) { .fullWidth {
position: absolute; @include mixins.header-pane;
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);
}
.pinnedMessage { .pinnedMessage {
margin-top: 0; margin-top: 0;
@ -75,12 +58,6 @@
.messageText { .messageText {
max-width: none; max-width: none;
} }
@media (min-width: 1276px) {
:global(#Main.right-column-open) & {
padding-left: calc(var(--right-column-width) + 1rem);
}
}
} }
.loading { .loading {
@ -244,30 +221,4 @@
max-width: none; 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; width: 22rem;
max-width: 100vw; max-width: 100vw;
margin: 4.25rem auto 0.25rem; margin: 4.25rem auto 0.25rem;
margin-top: calc(4.25rem + var(--middle-header-panes-height));
z-index: var(--z-notification); 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; 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_ICON_PLACEHOLDER = '⭐';
export const STARS_CURRENCY_CODE = 'XTR'; 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_RIGHT_COLUMN = 1275; // px
export const MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN = 925; // 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_MAX_WIDTH = 600; // px
export const MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH = 950; // px export const MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH = 950; // px
export const MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT = 450; // 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); setGlobal(global);
}); });
addActionHandler('hideChatReportPanel', async (global, actions, payload): Promise<void> => { addActionHandler('hideChatReportPane', async (global, actions, payload): Promise<void> => {
const { chatId } = payload!; const { chatId } = payload!;
const chat = selectChat(global, chatId); const chat = selectChat(global, chatId);
if (!chat) return; if (!chat) return;
const result = await callApi('hideChatReportPanel', chat); const result = await callApi('hideChatReportPane', chat);
if (!result) return; if (!result) return;
global = getGlobal(); global = getGlobal();

View File

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

View File

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

View File

@ -11,6 +11,7 @@ import type {
} from '../../api/types/messages'; } from '../../api/types/messages';
import type { OldLangFn } from '../../hooks/useOldLang'; import type { OldLangFn } from '../../hooks/useOldLang';
import type { ThreadId } from '../../types'; import type { ThreadId } from '../../types';
import type { LangFn } from '../../util/localization';
import type { GlobalState } from '../types'; import type { GlobalState } from '../types';
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/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); 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); return isPeerUser(sender) ? getUserFullName(sender) : getChatTitle(lang, sender);
} }

View File

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

View File

@ -2,6 +2,7 @@ import { useMemo } from '../lib/teact/teact';
import type { import type {
ApiAudio, ApiChat, ApiMessage, ApiPeer, ApiVoice, ApiAudio, ApiChat, ApiMessage, ApiPeer, ApiVoice,
MediaContent,
} from '../api/types'; } from '../api/types';
import { 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 // TODO Add support for video in future
const useMessageMediaMetadata = ( const useMessageMediaMetadata = (
message: ApiMessage, sender?: ApiPeer, chat?: ApiChat, message?: ApiMessage, sender?: ApiPeer, chat?: ApiChat,
): MediaMetadata | undefined => { ): MediaMetadata | undefined => {
const lang = useOldLang(); 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 title = audio ? (audio.title || audio.fileName) : voice ? 'Voice message' : '';
const artist = audio?.performer || (sender && getSenderTitle(lang, sender)); const artist = audio?.performer || (sender && getSenderTitle(lang, sender));
const album = (chat && getChatTitle(lang, chat)) || 'Telegram'; const album = (chat && getChatTitle(lang, chat)) || 'Telegram';

View File

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

View File

@ -117,21 +117,19 @@
} }
} }
@mixin header-mobile { @mixin header-pane {
position: absolute; position: absolute;
top: 100%; top: 0;
left: 0; width: 100%;
right: 0;
height: 2.875rem; 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-top: 0.375rem;
padding-bottom: 0.375rem; padding-bottom: 0.375rem;
padding-left: max(0.75rem, env(safe-area-inset-left)); padding-left: max(0.75rem, env(safe-area-inset-left));
padding-right: max(0.5rem, env(safe-area-inset-right)); padding-right: max(0.5rem, env(safe-area-inset-right));
background: var(--color-background); background-color: var(--color-background);
&::before { &::before {
content: ""; content: "";
@ -143,6 +141,18 @@
height: 0.125rem; height: 0.125rem;
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow); 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 { @mixin side-panel-section {

View File

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