Middle Header: Refactor subheader tools (#5182)
This commit is contained in:
parent
896c8b99a8
commit
5a647a0a83
@ -280,7 +280,7 @@ export function hideAllChatJoinRequests({
|
||||
});
|
||||
}
|
||||
|
||||
export function hideChatReportPanel(chat: ApiChat) {
|
||||
export function hideChatReportPane(chat: ApiChat) {
|
||||
const { id, accessHash } = chat;
|
||||
|
||||
return invokeRequest(new GramJs.messages.HidePeerSettingsBar({
|
||||
|
||||
@ -655,7 +655,11 @@
|
||||
"Statistics" = "Statistics";
|
||||
"EventLogFilterPinnedMessages" = "Pinned messages";
|
||||
"UnpinMessageAlertTitle" = "Unpin message";
|
||||
"PinnedMessage" = "Pinned message";
|
||||
"PinnedMessageTitleSingle" = "Pinned message";
|
||||
"PinnedMessageTitle_one" = "Pinned message #{index}";
|
||||
"PinnedMessageTitle_other" = "Pinned message #{index}";
|
||||
"AccPinnedMessages" = "Pinned Messages";
|
||||
"AccUnpinMessage" = "Unpin Message";
|
||||
"Comments_one" = "{count} Comment";
|
||||
"Comments_other" = "{count} Comments";
|
||||
"LeaveAComment" = "Leave a comment";
|
||||
|
||||
@ -1,37 +1,14 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.GroupCallTopPane {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 2.875rem;
|
||||
overflow: hidden;
|
||||
box-shadow: 0 2px 2px var(--color-light-shadow);
|
||||
@include mixins.header-pane;
|
||||
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 0.375rem 0.5rem 0.375rem 0.75rem;
|
||||
background: var(--color-background);
|
||||
z-index: -1;
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -0.1875rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0.125rem;
|
||||
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
&.has-pinned-offset {
|
||||
top: calc(100% + 2.875rem);
|
||||
}
|
||||
}
|
||||
|
||||
.info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
@ -56,9 +33,3 @@
|
||||
width: auto;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
#Main.right-column-open .MiddleHeader .GroupCallTopPane {
|
||||
width: calc(100% - var(--right-column-width));
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@ import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useShowTransitionDeprecated from '../../../hooks/useShowTransitionDeprecated';
|
||||
import useHeaderPane, { type PaneState } from '../../middle/hooks/useHeaderPane';
|
||||
|
||||
import AvatarList from '../../common/AvatarList';
|
||||
import Button from '../../ui/Button';
|
||||
@ -21,8 +21,8 @@ import './GroupCallTopPane.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
hasPinnedOffset: boolean;
|
||||
className?: string;
|
||||
onPaneStateChange?: (state: PaneState) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -37,7 +37,7 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
||||
isActive,
|
||||
className,
|
||||
groupCall,
|
||||
hasPinnedOffset,
|
||||
onPaneStateChange,
|
||||
}) => {
|
||||
const {
|
||||
requestMasterAndJoinGroupCall,
|
||||
@ -86,23 +86,24 @@ const GroupCallTopPane: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
}, [groupCall?.id, groupCall?.isLoaded, isActive, subscribeToGroupCallUpdates]);
|
||||
|
||||
const {
|
||||
shouldRender,
|
||||
transitionClassNames,
|
||||
} = useShowTransitionDeprecated(Boolean(groupCall && isActive));
|
||||
|
||||
const renderingParticipantCount = useCurrentOrPrev(groupCall?.participantsCount, true);
|
||||
const renderingFetchedParticipants = useCurrentOrPrev(fetchedParticipants, true);
|
||||
|
||||
const isRendering = Boolean(groupCall && isActive);
|
||||
|
||||
const { ref, shouldRender } = useHeaderPane({
|
||||
isOpen: isRendering,
|
||||
onStateChange: onPaneStateChange,
|
||||
});
|
||||
|
||||
if (!shouldRender) return undefined;
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName(
|
||||
'GroupCallTopPane',
|
||||
hasPinnedOffset && 'has-pinned-offset',
|
||||
className,
|
||||
transitionClassNames,
|
||||
)}
|
||||
onClick={handleJoinGroupCall}
|
||||
>
|
||||
|
||||
@ -37,6 +37,7 @@ type StateProps = {
|
||||
|
||||
const PinMessageModal: FC<OwnProps & StateProps> = ({
|
||||
isOpen,
|
||||
chatId,
|
||||
messageId,
|
||||
isChannel,
|
||||
isGroup,
|
||||
@ -49,17 +50,17 @@ const PinMessageModal: FC<OwnProps & StateProps> = ({
|
||||
|
||||
const handlePinMessageForAll = useCallback(() => {
|
||||
pinMessage({
|
||||
messageId, isUnpin: false,
|
||||
chatId, messageId, isUnpin: false,
|
||||
});
|
||||
onClose();
|
||||
}, [pinMessage, messageId, onClose]);
|
||||
}, [chatId, messageId, onClose]);
|
||||
|
||||
const handlePinMessage = useCallback(() => {
|
||||
pinMessage({
|
||||
messageId, isUnpin: false, isOneSide: true, isSilent: true,
|
||||
chatId, messageId, isUnpin: false, isOneSide: true, isSilent: true,
|
||||
});
|
||||
onClose();
|
||||
}, [messageId, onClose, pinMessage]);
|
||||
}, [chatId, messageId, onClose]);
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
|
||||
@ -43,10 +43,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
.group-call {
|
||||
position: static !important;
|
||||
}
|
||||
|
||||
.notch {
|
||||
width: 100%;
|
||||
height: 0;
|
||||
|
||||
@ -260,7 +260,7 @@ const ForumPanel: FC<OwnProps & StateProps> = ({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{chat && <GroupCallTopPane chatId={chat.id} hasPinnedOffset={false} className={styles.groupCall} />}
|
||||
{chat && <GroupCallTopPane chatId={chat.id} />}
|
||||
|
||||
<div className={styles.notch} />
|
||||
|
||||
|
||||
@ -8,7 +8,7 @@ import React, {
|
||||
import { addExtraClass } from '../../lib/teact/teact-dom';
|
||||
import { getActions, getGlobal, withGlobal } from '../../global';
|
||||
|
||||
import type { ApiChatFolder, ApiMessage, ApiUser } from '../../api/types';
|
||||
import type { ApiChatFolder, ApiUser } from '../../api/types';
|
||||
import type { ApiLimitTypeWithModal, TabState } from '../../global/types';
|
||||
import { ElectronEvent } from '../../types/electron';
|
||||
|
||||
@ -61,10 +61,10 @@ import StickerSetModal from '../common/StickerSetModal.async';
|
||||
import UnreadCount from '../common/UnreadCounter';
|
||||
import LeftColumn from '../left/LeftColumn';
|
||||
import MediaViewer from '../mediaViewer/MediaViewer.async';
|
||||
import AudioPlayer from '../middle/AudioPlayer';
|
||||
import ReactionPicker from '../middle/message/reactions/ReactionPicker.async';
|
||||
import MessageListHistoryHandler from '../middle/MessageListHistoryHandler';
|
||||
import MiddleColumn from '../middle/MiddleColumn';
|
||||
import AudioPlayer from '../middle/panes/AudioPlayer';
|
||||
import ModalContainer from '../modals/ModalContainer';
|
||||
import PaymentModal from '../payment/PaymentModal.async';
|
||||
import ReceiptModal from '../payment/ReceiptModal.async';
|
||||
@ -107,7 +107,6 @@ type StateProps = {
|
||||
isForwardModalOpen: boolean;
|
||||
hasNotifications: boolean;
|
||||
hasDialogs: boolean;
|
||||
audioMessage?: ApiMessage;
|
||||
safeLinkModalUrl?: string;
|
||||
isHistoryCalendarOpen: boolean;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
@ -159,7 +158,6 @@ const Main = ({
|
||||
isForwardModalOpen,
|
||||
hasNotifications,
|
||||
hasDialogs,
|
||||
audioMessage,
|
||||
activeGroupCallId,
|
||||
safeLinkModalUrl,
|
||||
isHistoryCalendarOpen,
|
||||
@ -545,7 +543,7 @@ const Main = ({
|
||||
<DraftRecipientPicker requestedDraft={requestedDraft} />
|
||||
<Notifications isOpen={hasNotifications} />
|
||||
<Dialogs isOpen={hasDialogs} />
|
||||
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
|
||||
<AudioPlayer noUi />
|
||||
<ModalContainer />
|
||||
<SafeLinkModal url={safeLinkModalUrl} />
|
||||
<HistoryCalendar isOpen={isHistoryCalendarOpen} />
|
||||
@ -613,7 +611,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
openedCustomEmojiSetIds,
|
||||
shouldSkipHistoryAnimations,
|
||||
openedGame,
|
||||
audioPlayer,
|
||||
isLeftColumnShown,
|
||||
historyCalendarSelectedAt,
|
||||
notifications,
|
||||
@ -630,10 +627,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
deleteFolderDialogModal,
|
||||
} = selectTabState(global);
|
||||
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
||||
const audioMessage = audioChatId && audioMessageId
|
||||
? selectChatMessage(global, audioChatId, audioMessageId)
|
||||
: undefined;
|
||||
const gameMessage = openedGame && selectChatMessage(global, openedGame.chatId, openedGame.messageId);
|
||||
const gameTitle = gameMessage?.content.game?.title;
|
||||
const { chatId } = selectCurrentMessageList(global) || {};
|
||||
@ -653,7 +646,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
isReactionPickerOpen: selectIsReactionPickerOpen(global),
|
||||
hasNotifications: Boolean(notifications.length),
|
||||
hasDialogs: Boolean(dialogs.length),
|
||||
audioMessage,
|
||||
safeLinkModalUrl,
|
||||
isHistoryCalendarOpen: Boolean(historyCalendarSelectedAt),
|
||||
shouldSkipHistoryAnimations,
|
||||
|
||||
@ -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;
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
@ -66,6 +66,10 @@
|
||||
}
|
||||
}
|
||||
|
||||
.first-message-date-group {
|
||||
padding-top: var(--middle-header-panes-height);
|
||||
}
|
||||
|
||||
&.no-composer {
|
||||
margin-bottom: 0;
|
||||
|
||||
@ -379,17 +383,13 @@
|
||||
|
||||
&.scrolled:not(.is-animating) .sticky-date {
|
||||
position: sticky;
|
||||
top: 0.625rem;
|
||||
top: calc(0.625rem + var(--middle-header-panes-height));
|
||||
}
|
||||
|
||||
&.is-animating .message-select-control {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.has-header-tools & .sticky-date {
|
||||
top: 3.75rem !important;
|
||||
}
|
||||
|
||||
.local-action-message,
|
||||
.ActionMessage {
|
||||
margin-top: 0.5rem;
|
||||
|
||||
@ -89,7 +89,6 @@ type OwnProps = {
|
||||
isReady: boolean;
|
||||
onScrollDownToggle: BooleanToVoidFunction;
|
||||
onNotchToggle: BooleanToVoidFunction;
|
||||
hasTools?: boolean;
|
||||
withBottomShift?: boolean;
|
||||
withDefaultBg: boolean;
|
||||
onIntersectPinnedMessage: OnIntersectPinnedMessage;
|
||||
@ -133,7 +132,6 @@ const MESSAGE_FACT_CHECK_UPDATE_INTERVAL = 5 * 1000;
|
||||
const MESSAGE_STORY_POLLING_INTERVAL = 5 * 60 * 1000;
|
||||
const BOTTOM_THRESHOLD = 50;
|
||||
const UNREAD_DIVIDER_TOP = 10;
|
||||
const UNREAD_DIVIDER_TOP_WITH_TOOLS = 60;
|
||||
const SCROLL_DEBOUNCE = 200;
|
||||
const MESSAGE_ANIMATION_DURATION = 500;
|
||||
const BOTTOM_FOCUS_MARGIN = 20;
|
||||
@ -146,7 +144,6 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
type,
|
||||
hasTools,
|
||||
isChatLoaded,
|
||||
isForum,
|
||||
isChannelChat,
|
||||
@ -405,7 +402,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
|
||||
if (!memoFocusingIdRef.current) {
|
||||
updateStickyDates(container, hasTools);
|
||||
updateStickyDates(container);
|
||||
}
|
||||
|
||||
runDebouncedForScroll(() => {
|
||||
@ -474,7 +471,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
useSyncEffect(
|
||||
() => forceMeasure(() => rememberScrollPositionRef.current()),
|
||||
// This will run before modifying content and should match deps for `useLayoutEffectWithPrevDeps` below
|
||||
[messageIds, isViewportNewest, hasTools, rememberScrollPositionRef],
|
||||
[messageIds, isViewportNewest, rememberScrollPositionRef],
|
||||
);
|
||||
useEffect(
|
||||
() => rememberScrollPositionRef.current(),
|
||||
@ -591,7 +588,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
newScrollTop = scrollTop + (newAnchorTop - (anchorTopRef.current || 0));
|
||||
} else if (unreadDivider) {
|
||||
newScrollTop = Math.min(
|
||||
unreadDivider.offsetTop - (hasTools ? UNREAD_DIVIDER_TOP_WITH_TOOLS : UNREAD_DIVIDER_TOP),
|
||||
unreadDivider.offsetTop - UNREAD_DIVIDER_TOP,
|
||||
scrollHeight - scrollOffset,
|
||||
);
|
||||
} else {
|
||||
@ -619,7 +616,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
||||
};
|
||||
});
|
||||
// This should match deps for `useSyncEffect` above
|
||||
}, [messageIds, isViewportNewest, hasTools, getContainerHeight, prevContainerHeightRef, noMessageSendingAnimation]);
|
||||
}, [messageIds, isViewportNewest, getContainerHeight, prevContainerHeightRef, noMessageSendingAnimation]);
|
||||
|
||||
useEffectWithPrevDeps(([prevIsSelectModeActive]) => {
|
||||
if (prevIsSelectModeActive !== undefined) {
|
||||
|
||||
@ -265,7 +265,7 @@ const MessageListContent: FC<OwnProps> = ({
|
||||
|
||||
return (
|
||||
<div
|
||||
className="message-date-group"
|
||||
className={buildClassName('message-date-group', dateGroupIndex === 0 && 'first-message-date-group')}
|
||||
key={dateGroup.datetime}
|
||||
onMouseDown={preventMessageInputBlur}
|
||||
teactFastList
|
||||
|
||||
@ -20,10 +20,6 @@ import {
|
||||
EDITABLE_INPUT_CSS_SELECTOR,
|
||||
EDITABLE_INPUT_ID,
|
||||
GENERAL_TOPIC_ID,
|
||||
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
|
||||
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
MOBILE_SCREEN_MAX_WIDTH,
|
||||
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
SUPPORTED_PHOTO_CONTENT_TYPES,
|
||||
TMP_CHAT_ID,
|
||||
} from '../../config';
|
||||
@ -45,7 +41,6 @@ import {
|
||||
selectCanAnimateInterface,
|
||||
selectChat,
|
||||
selectChatFullInfo,
|
||||
selectChatMessage,
|
||||
selectCurrentMessageList,
|
||||
selectCurrentMiddleSearch,
|
||||
selectDraft,
|
||||
@ -95,6 +90,7 @@ import FloatingActionButtons from './FloatingActionButtons';
|
||||
import MessageList from './MessageList';
|
||||
import MessageSelectToolbar from './MessageSelectToolbar.async';
|
||||
import MiddleHeader from './MiddleHeader';
|
||||
import MiddleHeaderPanes from './MiddleHeaderPanes';
|
||||
import PremiumRequiredPlaceholder from './PremiumRequiredPlaceholder';
|
||||
import ReactorListModal from './ReactorListModal.async';
|
||||
import MiddleSearch from './search/MiddleSearch.async';
|
||||
@ -119,9 +115,6 @@ type StateProps = {
|
||||
canPost?: boolean;
|
||||
currentUserBannedRights?: ApiChatBannedRights;
|
||||
defaultBannedRights?: ApiChatBannedRights;
|
||||
hasPinned?: boolean;
|
||||
hasAudioPlayer?: boolean;
|
||||
hasButtonInHeader?: boolean;
|
||||
pinnedMessagesCount?: number;
|
||||
theme: ThemeKey;
|
||||
customBackground?: string;
|
||||
@ -178,9 +171,6 @@ function MiddleColumn({
|
||||
canPost,
|
||||
currentUserBannedRights,
|
||||
defaultBannedRights,
|
||||
hasPinned,
|
||||
hasAudioPlayer,
|
||||
hasButtonInHeader,
|
||||
pinnedMessagesCount,
|
||||
customBackground,
|
||||
theme,
|
||||
@ -251,15 +241,6 @@ function MiddleColumn({
|
||||
} = usePinnedMessage(chatId, threadId, pinnedIds);
|
||||
|
||||
const closeAnimationDuration = isMobile ? LAYER_ANIMATION_DURATION_MS : undefined;
|
||||
const hasTools = hasPinned && (
|
||||
windowWidth < MOBILE_SCREEN_MAX_WIDTH
|
||||
|| hasAudioPlayer
|
||||
|| (
|
||||
isRightColumnShown && windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
||||
&& windowWidth < SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
||||
)
|
||||
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES)
|
||||
);
|
||||
|
||||
const renderingChatId = usePrevDuringAnimation(chatId, closeAnimationDuration);
|
||||
const renderingThreadId = usePrevDuringAnimation(threadId, closeAnimationDuration);
|
||||
@ -271,7 +252,6 @@ function MiddleColumn({
|
||||
const renderingCanPost = usePrevDuringAnimation(canPost, closeAnimationDuration)
|
||||
&& !renderingCanRestartBot && !renderingCanStartBot && !renderingCanSubscribe && !renderingCanUnblock
|
||||
&& chatId !== TMP_CHAT_ID && !isContactRequirePremium;
|
||||
const renderingHasTools = usePrevDuringAnimation(hasTools, closeAnimationDuration);
|
||||
const renderingIsScrollDownShown = usePrevDuringAnimation(
|
||||
isScrollDownShown, closeAnimationDuration,
|
||||
) && chatId !== TMP_CHAT_ID;
|
||||
@ -430,7 +410,6 @@ function MiddleColumn({
|
||||
const customBackgroundValue = useCustomBackground(theme, customBackground);
|
||||
|
||||
const className = buildClassName(
|
||||
renderingHasTools && 'has-header-tools',
|
||||
MASK_IMAGE_DISABLED ? 'mask-image-disabled' : 'mask-image-enabled',
|
||||
);
|
||||
|
||||
@ -522,12 +501,20 @@ function MiddleColumn({
|
||||
{Boolean(renderingChatId && renderingThreadId) && (
|
||||
<>
|
||||
<div className="messages-layout" onDragEnter={renderingCanPost ? handleDragEnter : undefined}>
|
||||
<MiddleHeaderPanes
|
||||
key={renderingChatId}
|
||||
chatId={renderingChatId!}
|
||||
threadId={renderingThreadId!}
|
||||
messageListType={renderingMessageListType!}
|
||||
getCurrentPinnedIndex={getCurrentPinnedIndex}
|
||||
getLoadingPinnedId={getLoadingPinnedId}
|
||||
onFocusPinnedMessage={handleFocusPinnedMessage}
|
||||
/>
|
||||
<MiddleHeader
|
||||
chatId={renderingChatId!}
|
||||
threadId={renderingThreadId!}
|
||||
messageListType={renderingMessageListType!}
|
||||
isComments={isComments}
|
||||
isReady={isReady}
|
||||
isMobile={isMobile}
|
||||
getCurrentPinnedIndex={getCurrentPinnedIndex}
|
||||
getLoadingPinnedId={getLoadingPinnedId}
|
||||
@ -548,7 +535,6 @@ function MiddleColumn({
|
||||
type={renderingMessageListType!}
|
||||
isComments={isComments}
|
||||
canPost={renderingCanPost!}
|
||||
hasTools={renderingHasTools}
|
||||
onScrollDownToggle={setIsScrollDownShown}
|
||||
onNotchToggle={setIsNotchShown}
|
||||
isReady={isReady}
|
||||
@ -728,7 +714,7 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const {
|
||||
messageLists, isLeftColumnShown, activeEmojiInteractions,
|
||||
seenByModal, reactorModal, audioPlayer, shouldSkipHistoryAnimations,
|
||||
seenByModal, reactorModal, shouldSkipHistoryAnimations,
|
||||
chatLanguageModal, privacySettingsNoticeModal,
|
||||
} = selectTabState(global);
|
||||
const currentMessageList = selectCurrentMessageList(global);
|
||||
@ -763,7 +749,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
const chat = selectChat(global, chatId);
|
||||
const bot = selectBot(global, chatId);
|
||||
const pinnedIds = selectPinnedIds(global, chatId, threadId);
|
||||
const { chatId: audioChatId, messageId: audioMessageId } = audioPlayer;
|
||||
const chatFullInfo = chatId ? selectChatFullInfo(global, chatId) : undefined;
|
||||
|
||||
const threadInfo = selectThreadInfo(global, chatId, threadId);
|
||||
@ -790,16 +775,11 @@ export default memo(withGlobal<OwnProps>(
|
||||
const shouldBlockSendInForum = chat?.isForum
|
||||
? threadId === MAIN_THREAD_ID && !draftReplyInfo && (selectTopic(global, chatId, GENERAL_TOPIC_ID)?.isClosed)
|
||||
: false;
|
||||
const audioMessage = audioChatId && audioMessageId
|
||||
? selectChatMessage(global, audioChatId, audioMessageId)
|
||||
: undefined;
|
||||
const topics = selectTopics(global, chatId);
|
||||
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
||||
const canShowOpenChatButton = isSavedDialog && threadId !== ANONYMOUS_USER_ID;
|
||||
|
||||
const isCommentThread = threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum;
|
||||
|
||||
const canUnpin = chat && (
|
||||
isPrivate || (
|
||||
chat?.isCreator || (!isChannel && !isUserRightBanned(chat, 'pinMessages'))
|
||||
@ -829,9 +809,6 @@ export default memo(withGlobal<OwnProps>(
|
||||
isPinnedMessageList,
|
||||
currentUserBannedRights: chat?.currentUserBannedRights,
|
||||
defaultBannedRights: chat?.defaultBannedRights,
|
||||
hasPinned: isCommentThread || Boolean(!isPinnedMessageList && pinnedIds?.length),
|
||||
hasAudioPlayer: Boolean(audioMessage),
|
||||
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
|
||||
pinnedMessagesCount: pinnedIds ? pinnedIds.length : 0,
|
||||
shouldSkipHistoryAnimations,
|
||||
isChannel,
|
||||
|
||||
@ -1,33 +1,9 @@
|
||||
@use "../../styles/mixins";
|
||||
|
||||
@mixin mobile-header-styles() {
|
||||
.AudioPlayer {
|
||||
@include mixins.header-mobile;
|
||||
|
||||
flex-direction: row;
|
||||
margin-top: 0;
|
||||
padding: 0.25rem 0.5rem;
|
||||
|
||||
&-content {
|
||||
padding: 0 0.5rem;
|
||||
flex-grow: 1;
|
||||
}
|
||||
|
||||
> .Button, > .playback-rate-menu {
|
||||
margin: -0.0625rem 0 0;
|
||||
}
|
||||
|
||||
> .player-close {
|
||||
margin-left: auto;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.MiddleHeader {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
width: 100%;
|
||||
box-shadow: 0 2px 2px var(--color-light-shadow);
|
||||
background: var(--color-background);
|
||||
position: relative;
|
||||
z-index: var(--z-middle-header);
|
||||
@ -118,18 +94,7 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1276px) and (max-width: 1439px) {
|
||||
.HeaderActions {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform var(--layer-transition);
|
||||
|
||||
#Main.right-column-open & {
|
||||
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
@media (min-width: 1276px) {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: transform var(--layer-transition);
|
||||
|
||||
@ -144,34 +109,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1276px) and (max-width: 1439px) {
|
||||
&:not(.tools-stacked) .AudioPlayer {
|
||||
opacity: 1;
|
||||
|
||||
#Main.right-column-open & {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tools-stacked .AudioPlayer {
|
||||
@include mobile-header-styles();
|
||||
|
||||
@media (min-width: 1150px) {
|
||||
#Main.right-column-open & {
|
||||
padding-right: calc(0.5rem + var(--right-column-width));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.tools-stacked.animated .AudioPlayer {
|
||||
animation: fade-in var(--layer-transition) forwards;
|
||||
|
||||
body.no-page-transitions & {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
h3 {
|
||||
font-weight: 500;
|
||||
font-size: 1.125rem;
|
||||
@ -384,10 +321,6 @@
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
@include mobile-header-styles();
|
||||
}
|
||||
|
||||
body.is-electron.is-macos & {
|
||||
-webkit-app-region: drag;
|
||||
}
|
||||
|
||||
@ -1,11 +1,11 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useLayoutEffect, useRef,
|
||||
memo, useRef,
|
||||
} from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
|
||||
import type {
|
||||
ApiChat, ApiMessage, ApiPeer, ApiSticker, ApiTypingStatus,
|
||||
ApiChat, ApiMessage, ApiSticker, ApiTypingStatus,
|
||||
} from '../../api/types';
|
||||
import type { GlobalState, MessageListType } from '../../global/types';
|
||||
import type { Signal } from '../../util/signals';
|
||||
@ -14,35 +14,18 @@ import { StoryViewerOrigin, type ThreadId } from '../../types';
|
||||
|
||||
import {
|
||||
EDITABLE_INPUT_CSS_SELECTOR,
|
||||
MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES,
|
||||
MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN,
|
||||
MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
MOBILE_SCREEN_MAX_WIDTH,
|
||||
SAFE_SCREEN_WIDTH_FOR_CHAT_INFO,
|
||||
SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN,
|
||||
} from '../../config';
|
||||
import { requestMutation } from '../../lib/fasterdom/fasterdom';
|
||||
import {
|
||||
getChatTitle,
|
||||
getIsSavedDialog,
|
||||
getSenderTitle,
|
||||
isChatChannel,
|
||||
isChatSuperGroup,
|
||||
isUserId,
|
||||
} from '../../global/helpers';
|
||||
import {
|
||||
selectAllowedMessageActionsSlow,
|
||||
selectChat,
|
||||
selectChatMessage,
|
||||
selectChatMessages,
|
||||
selectCurrentMiddleSearch,
|
||||
selectForwardedSender,
|
||||
selectIsChatBotNotStarted,
|
||||
selectIsChatWithBot,
|
||||
selectIsChatWithSelf,
|
||||
selectIsInSelectMode,
|
||||
selectIsRightColumnShown,
|
||||
selectIsUserBlocked,
|
||||
selectPinnedIds,
|
||||
selectScheduledIds,
|
||||
selectTabState,
|
||||
@ -50,36 +33,27 @@ import {
|
||||
selectThreadParam,
|
||||
} from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import cycleRestrict from '../../util/cycleRestrict';
|
||||
import { getMessageKey } from '../../util/keys/messageKey';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useConnectionStatus from '../../hooks/useConnectionStatus';
|
||||
import useCurrentOrPrev from '../../hooks/useCurrentOrPrev';
|
||||
import useDerivedState from '../../hooks/useDerivedState';
|
||||
import useElectronDrag from '../../hooks/useElectronDrag';
|
||||
import useEnsureMessage from '../../hooks/useEnsureMessage';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useLongPress from '../../hooks/useLongPress';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import usePreviousDeprecated from '../../hooks/usePreviousDeprecated';
|
||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||
import useWindowSize from '../../hooks/window/useWindowSize';
|
||||
|
||||
import GroupCallTopPane from '../calls/group/GroupCallTopPane';
|
||||
import GroupChatInfo from '../common/GroupChatInfo';
|
||||
import PrivateChatInfo from '../common/PrivateChatInfo';
|
||||
import UnreadCounter from '../common/UnreadCounter';
|
||||
import Button from '../ui/Button';
|
||||
import Transition from '../ui/Transition';
|
||||
import AudioPlayer from './AudioPlayer';
|
||||
import ChatReportPanel from './ChatReportPanel';
|
||||
import HeaderActions from './HeaderActions';
|
||||
import HeaderPinnedMessage from './HeaderPinnedMessage';
|
||||
import AudioPlayer from './panes/AudioPlayer';
|
||||
import HeaderPinnedMessage from './panes/HeaderPinnedMessage';
|
||||
|
||||
import './MiddleHeader.scss';
|
||||
|
||||
const ANIMATION_DURATION = 350;
|
||||
const BACK_BUTTON_INACTIVE_TIME = 450;
|
||||
const EMOJI_STATUS_SIZE = 22;
|
||||
const SEARCH_LONGTAP_THRESHOLD = 500;
|
||||
@ -89,7 +63,6 @@ type OwnProps = {
|
||||
threadId: ThreadId;
|
||||
messageListType: MessageListType;
|
||||
isComments?: boolean;
|
||||
isReady?: boolean;
|
||||
isMobile?: boolean;
|
||||
getCurrentPinnedIndex: Signal<number>;
|
||||
getLoadingPinnedId: Signal<number | undefined>;
|
||||
@ -98,11 +71,7 @@ type OwnProps = {
|
||||
|
||||
type StateProps = {
|
||||
chat?: ApiChat;
|
||||
pinnedMessageIds?: number[] | number;
|
||||
messagesById?: Record<number, ApiMessage>;
|
||||
canUnpin?: boolean;
|
||||
isSavedDialog?: boolean;
|
||||
topMessageSender?: ApiPeer;
|
||||
typingStatus?: ApiTypingStatus;
|
||||
isSelectModeActive?: boolean;
|
||||
isLeftColumnShown?: boolean;
|
||||
@ -110,61 +79,45 @@ type StateProps = {
|
||||
audioMessage?: ApiMessage;
|
||||
messagesCount?: number;
|
||||
isChatWithSelf?: boolean;
|
||||
hasButtonInHeader?: boolean;
|
||||
shouldSkipHistoryAnimations?: boolean;
|
||||
currentTransitionKey: number;
|
||||
connectionState?: GlobalState['connectionState'];
|
||||
isSyncing?: boolean;
|
||||
isSynced?: boolean;
|
||||
isFetchingDifference?: boolean;
|
||||
emojiStatusSticker?: ApiSticker;
|
||||
isMiddleSearchOpen?: boolean;
|
||||
};
|
||||
|
||||
const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
threadId,
|
||||
messageListType,
|
||||
isReady,
|
||||
isMobile,
|
||||
pinnedMessageIds,
|
||||
messagesById,
|
||||
canUnpin,
|
||||
topMessageSender,
|
||||
typingStatus,
|
||||
isSelectModeActive,
|
||||
isLeftColumnShown,
|
||||
isRightColumnShown,
|
||||
audioMessage,
|
||||
chat,
|
||||
messagesCount,
|
||||
isComments,
|
||||
isChatWithSelf,
|
||||
hasButtonInHeader,
|
||||
shouldSkipHistoryAnimations,
|
||||
currentTransitionKey,
|
||||
connectionState,
|
||||
isSyncing,
|
||||
isSynced,
|
||||
isFetchingDifference,
|
||||
getCurrentPinnedIndex,
|
||||
getLoadingPinnedId,
|
||||
emojiStatusSticker,
|
||||
isSavedDialog,
|
||||
isMiddleSearchOpen,
|
||||
onFocusPinnedMessage,
|
||||
}) => {
|
||||
const {
|
||||
openThreadWithInfo,
|
||||
pinMessage,
|
||||
focusMessage,
|
||||
openChat,
|
||||
openPreviousChat,
|
||||
loadPinnedMessages,
|
||||
toggleLeftColumn,
|
||||
exitMessageSelectMode,
|
||||
openPremiumModal,
|
||||
openThread,
|
||||
openStickerSet,
|
||||
updateMiddleSearch,
|
||||
} = getActions();
|
||||
@ -173,32 +126,15 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
const isBackButtonActive = useRef(true);
|
||||
const { isTablet } = useAppLayout();
|
||||
|
||||
const currentPinnedIndex = useDerivedState(getCurrentPinnedIndex);
|
||||
const pinnedMessageId = Array.isArray(pinnedMessageIds) ? pinnedMessageIds[currentPinnedIndex] : pinnedMessageIds;
|
||||
const pinnedMessage = messagesById && pinnedMessageId ? messagesById[pinnedMessageId] : undefined;
|
||||
const pinnedMessagesCount = Array.isArray(pinnedMessageIds)
|
||||
? pinnedMessageIds.length : (pinnedMessageIds ? 1 : undefined);
|
||||
const chatTitleLength = chat && getChatTitle(lang, chat).length;
|
||||
const topMessageTitle = topMessageSender ? getSenderTitle(lang, topMessageSender) : undefined;
|
||||
const { settings } = chat || {};
|
||||
const isForum = chat?.isForum;
|
||||
|
||||
useEffect(() => {
|
||||
if (isSynced && isReady && (threadId === MAIN_THREAD_ID || isForum)) {
|
||||
loadPinnedMessages({ chatId, threadId });
|
||||
}
|
||||
}, [chatId, threadId, isSynced, isReady, isForum]);
|
||||
|
||||
useEnsureMessage(chatId, pinnedMessageId, pinnedMessage);
|
||||
|
||||
const { width: windowWidth } = useWindowSize();
|
||||
|
||||
const { isDesktop } = useAppLayout();
|
||||
|
||||
const isLeftColumnHideable = windowWidth <= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN;
|
||||
const shouldShowCloseButton = isTablet && isLeftColumnShown;
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const componentRef = useRef<HTMLDivElement>(null);
|
||||
const shouldAnimateTools = useRef<boolean>(true);
|
||||
|
||||
const handleOpenSearch = useLastCallback(() => {
|
||||
updateMiddleSearch({ chatId, threadId, update: {} });
|
||||
@ -222,27 +158,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
threshold: SEARCH_LONGTAP_THRESHOLD,
|
||||
});
|
||||
|
||||
const handleUnpinMessage = useLastCallback((messageId: number) => {
|
||||
pinMessage({ messageId, isUnpin: true });
|
||||
});
|
||||
|
||||
const handlePinnedMessageClick = useLastCallback((e: React.MouseEvent<HTMLElement, MouseEvent>): void => {
|
||||
const messageId = e.shiftKey && Array.isArray(pinnedMessageIds)
|
||||
? pinnedMessageIds[cycleRestrict(pinnedMessageIds.length, pinnedMessageIds.indexOf(pinnedMessageId!) - 2)]
|
||||
: pinnedMessageId!;
|
||||
|
||||
if (!getLoadingPinnedId()) {
|
||||
focusMessage({
|
||||
chatId, threadId, messageId, noForumTopicPanel: true,
|
||||
});
|
||||
onFocusPinnedMessage(messageId);
|
||||
}
|
||||
});
|
||||
|
||||
const handleAllPinnedClick = useLastCallback(() => {
|
||||
openThread({ chatId, threadId, type: 'pinned' });
|
||||
});
|
||||
|
||||
const setBackButtonActive = useLastCallback(() => {
|
||||
setTimeout(() => {
|
||||
isBackButtonActive.current = true;
|
||||
@ -292,81 +207,14 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
setBackButtonActive();
|
||||
});
|
||||
|
||||
const canToolsCollideWithChatInfo = (
|
||||
windowWidth >= MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN
|
||||
&& windowWidth < SAFE_SCREEN_WIDTH_FOR_CHAT_INFO
|
||||
) || (
|
||||
windowWidth > MOBILE_SCREEN_MAX_WIDTH
|
||||
&& windowWidth < MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN
|
||||
&& (!chatTitleLength || chatTitleLength > 30)
|
||||
);
|
||||
const shouldUseStackedToolsClass = canToolsCollideWithChatInfo || (
|
||||
windowWidth > MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
||||
&& windowWidth < SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN
|
||||
);
|
||||
|
||||
const hasChatSettings = Boolean(settings?.canAddContact || settings?.canBlockContact || settings?.canReportSpam);
|
||||
const {
|
||||
shouldRender: shouldShowChatReportPanel,
|
||||
transitionClassNames: chatReportPanelClassNames,
|
||||
} = useShowTransitionDeprecated(hasChatSettings);
|
||||
const renderingChatSettings = useCurrentOrPrev(hasChatSettings ? settings : undefined, true);
|
||||
|
||||
const {
|
||||
shouldRender: shouldRenderAudioPlayer,
|
||||
transitionClassNames: audioPlayerClassNames,
|
||||
} = useShowTransitionDeprecated(Boolean(audioMessage));
|
||||
|
||||
const renderingAudioMessage = useCurrentOrPrev(audioMessage, true);
|
||||
|
||||
const {
|
||||
shouldRender: shouldRenderPinnedMessage,
|
||||
transitionClassNames: pinnedMessageClassNames,
|
||||
} = useShowTransitionDeprecated(Boolean(pinnedMessage) && !isMiddleSearchOpen, undefined, true);
|
||||
|
||||
const renderingPinnedMessage = useCurrentOrPrev(pinnedMessage, true);
|
||||
const renderingPinnedMessagesCount = useCurrentOrPrev(pinnedMessagesCount, true);
|
||||
const renderingCanUnpin = useCurrentOrPrev(canUnpin, true);
|
||||
const renderingPinnedMessageTitle = useCurrentOrPrev(topMessageTitle);
|
||||
|
||||
const prevTransitionKey = usePreviousDeprecated(currentTransitionKey);
|
||||
const cleanupExceptionKey = (
|
||||
prevTransitionKey !== undefined && prevTransitionKey < currentTransitionKey ? prevTransitionKey : undefined
|
||||
);
|
||||
|
||||
const canRevealTools = (shouldRenderPinnedMessage && renderingPinnedMessage)
|
||||
|| (shouldRenderAudioPlayer && renderingAudioMessage);
|
||||
|
||||
// Logic for transition to and from custom display of AudioPlayer/PinnedMessage on smaller screens
|
||||
useLayoutEffect(() => {
|
||||
const componentEl = componentRef.current;
|
||||
if (!componentEl) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldUseStackedToolsClass || !canRevealTools) {
|
||||
componentEl.classList.remove('tools-stacked', 'animated');
|
||||
shouldAnimateTools.current = true;
|
||||
return;
|
||||
}
|
||||
|
||||
if (isRightColumnShown || canToolsCollideWithChatInfo) {
|
||||
if (shouldAnimateTools.current) {
|
||||
componentEl.classList.add('tools-stacked', 'animated');
|
||||
shouldAnimateTools.current = false;
|
||||
}
|
||||
|
||||
// Remove animation class to prevent it messing up the show transitions
|
||||
setTimeout(() => {
|
||||
requestMutation(() => {
|
||||
componentEl.classList.remove('animated');
|
||||
});
|
||||
}, ANIMATION_DURATION);
|
||||
} else {
|
||||
componentEl.classList.remove('tools-stacked');
|
||||
shouldAnimateTools.current = true;
|
||||
}
|
||||
}, [shouldUseStackedToolsClass, canRevealTools, canToolsCollideWithChatInfo, isRightColumnShown]);
|
||||
const isAudioPlayerActive = Boolean(audioMessage);
|
||||
const isAudioPlayerRendering = isDesktop && isAudioPlayerActive;
|
||||
const isPinnedMessagesFullWidth = isAudioPlayerActive || !isDesktop;
|
||||
|
||||
const { connectionStatusText } = useConnectionStatus(lang, connectionState, isSyncing || isFetchingDifference, true);
|
||||
|
||||
@ -470,10 +318,6 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
);
|
||||
}
|
||||
|
||||
const isAudioPlayerRendered = Boolean(shouldRenderAudioPlayer && renderingAudioMessage);
|
||||
const isPinnedMessagesFullWidth = isAudioPlayerRendered
|
||||
|| (!isMobile && hasButtonInHeader && windowWidth < MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES);
|
||||
|
||||
useElectronDrag(componentRef);
|
||||
|
||||
return (
|
||||
@ -486,56 +330,28 @@ const MiddleHeader: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
{renderInfo()}
|
||||
</Transition>
|
||||
|
||||
{threadId === MAIN_THREAD_ID && !chat?.isForum && (
|
||||
<GroupCallTopPane
|
||||
hasPinnedOffset={
|
||||
(shouldRenderPinnedMessage && Boolean(renderingPinnedMessage))
|
||||
|| (shouldRenderAudioPlayer && Boolean(renderingAudioMessage))
|
||||
}
|
||||
chatId={chatId}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldRenderPinnedMessage && renderingPinnedMessage && (
|
||||
{!isPinnedMessagesFullWidth && (
|
||||
<HeaderPinnedMessage
|
||||
key={chatId}
|
||||
message={renderingPinnedMessage}
|
||||
count={renderingPinnedMessagesCount || 0}
|
||||
index={currentPinnedIndex}
|
||||
customTitle={renderingPinnedMessageTitle}
|
||||
className={pinnedMessageClassNames}
|
||||
onUnpinMessage={renderingCanUnpin ? handleUnpinMessage : undefined}
|
||||
onClick={handlePinnedMessageClick}
|
||||
onAllPinnedClick={handleAllPinnedClick}
|
||||
getLoadingPinnedId={getLoadingPinnedId}
|
||||
isFullWidth={isPinnedMessagesFullWidth}
|
||||
/>
|
||||
)}
|
||||
|
||||
{shouldShowChatReportPanel && (
|
||||
<ChatReportPanel
|
||||
key={chatId}
|
||||
chatId={chatId}
|
||||
settings={renderingChatSettings}
|
||||
className={chatReportPanelClassNames}
|
||||
threadId={threadId}
|
||||
messageListType={messageListType}
|
||||
onFocusPinnedMessage={onFocusPinnedMessage}
|
||||
getLoadingPinnedId={getLoadingPinnedId}
|
||||
getCurrentPinnedIndex={getCurrentPinnedIndex}
|
||||
/>
|
||||
)}
|
||||
|
||||
<div className="header-tools">
|
||||
{isAudioPlayerRendered && (
|
||||
<AudioPlayer
|
||||
key={getMessageKey(renderingAudioMessage!)}
|
||||
message={renderingAudioMessage!}
|
||||
className={audioPlayerClassNames}
|
||||
/>
|
||||
{isAudioPlayerRendering && (
|
||||
<AudioPlayer />
|
||||
)}
|
||||
<HeaderActions
|
||||
chatId={chatId}
|
||||
threadId={threadId}
|
||||
messageListType={messageListType}
|
||||
isMobile={isMobile}
|
||||
canExpandActions={!isAudioPlayerRendered}
|
||||
canExpandActions={!isAudioPlayerRendering}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@ -568,23 +384,14 @@ export default memo(withGlobal<OwnProps>(
|
||||
messagesCount = threadInfo?.messagesCount || 0;
|
||||
}
|
||||
|
||||
const isMainThread = messageListType === 'thread' && threadId === MAIN_THREAD_ID;
|
||||
const isChatWithBot = chat && selectIsChatWithBot(global, chat);
|
||||
const canRestartBot = Boolean(isChatWithBot && selectIsUserBlocked(global, chatId));
|
||||
const canStartBot = isChatWithBot && !canRestartBot && Boolean(selectIsChatBotNotStarted(global, chatId));
|
||||
const canSubscribe = Boolean(
|
||||
chat && (isMainThread || chat.isForum) && (isChatChannel(chat) || isChatSuperGroup(chat)) && chat.isNotJoined,
|
||||
);
|
||||
const shouldSendJoinRequest = Boolean(chat?.isNotJoined && chat.isJoinRequest);
|
||||
const typingStatus = selectThreadParam(global, chatId, threadId, 'typingStatus');
|
||||
|
||||
const emojiStatus = chat?.emojiStatus;
|
||||
const emojiStatusSticker = emojiStatus && global.customEmojis.byId[emojiStatus.documentId];
|
||||
|
||||
const isSavedDialog = getIsSavedDialog(chatId, threadId, global.currentUserId);
|
||||
const isMiddleSearchOpen = Boolean(selectCurrentMiddleSearch(global));
|
||||
|
||||
const state: StateProps = {
|
||||
return {
|
||||
typingStatus,
|
||||
isLeftColumnShown,
|
||||
isRightColumnShown: selectIsRightColumnShown(global, isMobile),
|
||||
@ -597,52 +404,9 @@ export default memo(withGlobal<OwnProps>(
|
||||
currentTransitionKey: Math.max(0, messageLists.length - 1),
|
||||
connectionState: global.connectionState,
|
||||
isSyncing: global.isSyncing,
|
||||
isSynced: global.isSynced,
|
||||
isFetchingDifference: global.isFetchingDifference,
|
||||
emojiStatusSticker,
|
||||
hasButtonInHeader: canStartBot || canRestartBot || canSubscribe || shouldSendJoinRequest,
|
||||
isSavedDialog,
|
||||
isMiddleSearchOpen,
|
||||
};
|
||||
|
||||
const messagesById = selectChatMessages(global, chatId);
|
||||
if (messageListType !== 'thread' || !messagesById) {
|
||||
return state;
|
||||
}
|
||||
|
||||
if (threadId !== MAIN_THREAD_ID && !isSavedDialog && !chat?.isForum) {
|
||||
const pinnedMessageId = Number(threadId);
|
||||
const message = pinnedMessageId ? selectChatMessage(global, chatId, pinnedMessageId) : undefined;
|
||||
const topMessageSender = message ? selectForwardedSender(global, message) : undefined;
|
||||
|
||||
return {
|
||||
...state,
|
||||
pinnedMessageIds: pinnedMessageId,
|
||||
messagesById,
|
||||
canUnpin: false,
|
||||
topMessageSender,
|
||||
};
|
||||
}
|
||||
|
||||
const pinnedMessageIds = !isSavedDialog ? selectPinnedIds(global, chatId, threadId) : undefined;
|
||||
if (pinnedMessageIds?.length) {
|
||||
const firstPinnedMessage = messagesById[pinnedMessageIds[0]];
|
||||
const {
|
||||
canUnpin = false,
|
||||
} = (
|
||||
firstPinnedMessage
|
||||
&& pinnedMessageIds.length === 1
|
||||
&& selectAllowedMessageActionsSlow(global, firstPinnedMessage, threadId)
|
||||
) || {};
|
||||
|
||||
return {
|
||||
...state,
|
||||
pinnedMessageIds,
|
||||
messagesById,
|
||||
canUnpin,
|
||||
};
|
||||
}
|
||||
|
||||
return state;
|
||||
},
|
||||
)(MiddleHeader));
|
||||
|
||||
36
src/components/middle/MiddleHeaderPanes.module.scss
Normal file
36
src/components/middle/MiddleHeaderPanes.module.scss
Normal 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
|
||||
}
|
||||
}
|
||||
163
src/components/middle/MiddleHeaderPanes.tsx
Normal file
163
src/components/middle/MiddleHeaderPanes.tsx
Normal 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));
|
||||
@ -1,6 +1,7 @@
|
||||
import React, { type TeactNode } from '../../../../lib/teact/teact';
|
||||
|
||||
import type { ApiKeyboardButton } from '../../../../api/types';
|
||||
import type { LangFn } from '../../../../util/localization';
|
||||
|
||||
import { STARS_ICON_PLACEHOLDER } from '../../../../config';
|
||||
import { replaceWithTeact } from '../../../../util/replaceWithTeact';
|
||||
@ -10,7 +11,7 @@ import { type OldLangFn } from '../../../../hooks/useOldLang';
|
||||
|
||||
import Icon from '../../../common/icons/Icon';
|
||||
|
||||
export default function renderKeyboardButtonText(lang: OldLangFn, button: ApiKeyboardButton): TeactNode {
|
||||
export default function renderKeyboardButtonText(lang: OldLangFn | LangFn, button: ApiKeyboardButton): TeactNode {
|
||||
if (button.type === 'receipt') {
|
||||
return lang('PaymentReceipt');
|
||||
}
|
||||
|
||||
133
src/components/middle/hooks/useHeaderPane.tsx
Normal file
133
src/components/middle/hooks/useHeaderPane.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -6,7 +6,6 @@ import useRunDebounced from '../../../hooks/useRunDebounced';
|
||||
|
||||
const DEBOUNCE = 1000;
|
||||
const STICKY_TOP = 10;
|
||||
const STICKY_TOP_WITH_TOOLS = 60;
|
||||
|
||||
export default function useStickyDates() {
|
||||
// For some reason we can not synchronously hide a sticky element (from `useLayoutEffect`) when chat opens
|
||||
@ -15,7 +14,7 @@ export default function useStickyDates() {
|
||||
|
||||
const runDebounced = useRunDebounced(DEBOUNCE, true);
|
||||
|
||||
const updateStickyDates = useLastCallback((container: HTMLDivElement, hasTools?: boolean) => {
|
||||
const updateStickyDates = useLastCallback((container: HTMLDivElement) => {
|
||||
markIsScrolled();
|
||||
|
||||
if (!document.body.classList.contains('is-scrolling-messages')) {
|
||||
@ -25,7 +24,7 @@ export default function useStickyDates() {
|
||||
}
|
||||
|
||||
runDebounced(() => {
|
||||
const stuckDateEl = findStuckDate(container, hasTools);
|
||||
const stuckDateEl = findStuckDate(container);
|
||||
if (stuckDateEl) {
|
||||
requestMutation(() => {
|
||||
stuckDateEl.classList.add('stuck');
|
||||
@ -49,13 +48,16 @@ export default function useStickyDates() {
|
||||
};
|
||||
}
|
||||
|
||||
function findStuckDate(container: HTMLElement, hasTools?: boolean) {
|
||||
function findStuckDate(container: HTMLElement) {
|
||||
const allElements = container.querySelectorAll<HTMLDivElement>('.sticky-date');
|
||||
const containerTop = container.scrollTop;
|
||||
|
||||
const computedStyle = getComputedStyle(container);
|
||||
const headerActionsHeight = parseInt(computedStyle.getPropertyValue('--middle-header-panes-height'), 10);
|
||||
|
||||
return Array.from(allElements).find((el) => {
|
||||
const { offsetTop, offsetHeight } = el;
|
||||
const top = offsetTop - containerTop;
|
||||
return -offsetHeight <= top && top <= (hasTools ? STICKY_TOP_WITH_TOOLS : STICKY_TOP);
|
||||
return -offsetHeight <= top && top <= (headerActionsHeight || STICKY_TOP);
|
||||
});
|
||||
}
|
||||
|
||||
@ -406,7 +406,7 @@ const ContextMenuContainer: FC<OwnProps & StateProps> = ({
|
||||
});
|
||||
|
||||
const handleUnpin = useLastCallback(() => {
|
||||
pinMessage({ messageId: message.id, isUnpin: true });
|
||||
pinMessage({ chatId: message.chatId, messageId: message.id, isUnpin: true });
|
||||
closeMenu();
|
||||
});
|
||||
|
||||
|
||||
@ -312,10 +312,10 @@ const MessageContextMenu: FC<OwnProps> = ({
|
||||
const getLayout = useLastCallback(() => {
|
||||
const extraHeightAudioPlayer = (isMobile
|
||||
&& (document.querySelector<HTMLElement>('.AudioPlayer-content'))?.offsetHeight) || 0;
|
||||
const pinnedElement = document.querySelector<HTMLElement>('.HeaderPinnedMessageWrapper');
|
||||
const extraHeightPinned = (((isMobile && !extraHeightAudioPlayer)
|
||||
|| (!isMobile && pinnedElement?.classList.contains('full-width')))
|
||||
&& pinnedElement?.offsetHeight) || 0;
|
||||
const middleColumn = document.getElementById('MiddleColumn')!;
|
||||
const middleColumnComputedStyle = getComputedStyle(middleColumn);
|
||||
const headerToolsHeight = parseFloat(middleColumnComputedStyle.getPropertyValue('--middle-header-panes-height'));
|
||||
const extraHeightPinned = headerToolsHeight || 0;
|
||||
|
||||
return {
|
||||
extraPaddingX: SCROLLBAR_WIDTH,
|
||||
|
||||
@ -1,8 +1,9 @@
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.AudioPlayer {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-left: auto;
|
||||
margin-top: -0.25rem;
|
||||
margin-bottom: -0.25rem;
|
||||
|
||||
body.no-page-transitions & {
|
||||
transition: none !important;
|
||||
@ -170,10 +171,6 @@
|
||||
max-width: 10rem;
|
||||
}
|
||||
|
||||
@media (max-width: 600px) {
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
@media (min-width: 1440px) {
|
||||
max-width: 24rem;
|
||||
.right-column-shown & {
|
||||
@ -213,6 +210,15 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.full-width-player {
|
||||
@include mixins.header-pane;
|
||||
|
||||
.AudioPlayer-content {
|
||||
max-width: none;
|
||||
flex-grow: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.playback-rate-menu .bubble {
|
||||
min-width: auto;
|
||||
|
||||
@ -1,47 +1,54 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { useMemo, useRef } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { useMemo } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiAudio, ApiChat, ApiMessage, ApiPeer,
|
||||
} from '../../api/types';
|
||||
import type { AudioOrigin } from '../../types';
|
||||
MediaContent,
|
||||
} from '../../../api/types';
|
||||
|
||||
import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../config';
|
||||
import { PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION } from '../../../config';
|
||||
import {
|
||||
getMediaDuration, getMessageContent, getMessageMediaHash, getSenderTitle, isMessageLocal,
|
||||
} from '../../global/helpers';
|
||||
import { selectChat, selectSender, selectTabState } from '../../global/selectors';
|
||||
import { makeTrackId } from '../../util/audioPlayer';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import * as mediaLoader from '../../util/mediaLoader';
|
||||
import { clearMediaSession } from '../../util/mediaSession';
|
||||
import { IS_IOS, IS_TOUCH_ENV } from '../../util/windowEnvironment';
|
||||
import renderText from '../common/helpers/renderText';
|
||||
} from '../../../global/helpers';
|
||||
import {
|
||||
selectChat, selectChatMessage, selectSender, selectTabState,
|
||||
} from '../../../global/selectors';
|
||||
import { makeTrackId } from '../../../util/audioPlayer';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
import * as mediaLoader from '../../../util/mediaLoader';
|
||||
import { clearMediaSession } from '../../../util/mediaSession';
|
||||
import { IS_IOS, IS_TOUCH_ENV } from '../../../util/windowEnvironment';
|
||||
import renderText from '../../common/helpers/renderText';
|
||||
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMessageMediaMetadata from '../../hooks/useMessageMediaMetadata';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useAppLayout from '../../../hooks/useAppLayout';
|
||||
import useAudioPlayer from '../../../hooks/useAudioPlayer';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMessageMediaMetadata from '../../../hooks/useMessageMediaMetadata';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import DropdownMenu from '../ui/DropdownMenu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import RangeSlider from '../ui/RangeSlider';
|
||||
import RippleEffect from '../ui/RippleEffect';
|
||||
import Button from '../../ui/Button';
|
||||
import DropdownMenu from '../../ui/DropdownMenu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import RangeSlider from '../../ui/RangeSlider';
|
||||
import RippleEffect from '../../ui/RippleEffect';
|
||||
|
||||
import './AudioPlayer.scss';
|
||||
|
||||
type OwnProps = {
|
||||
message: ApiMessage;
|
||||
origin?: AudioOrigin;
|
||||
className?: string;
|
||||
noUi?: boolean;
|
||||
isFullWidth?: boolean;
|
||||
isHidden?: boolean;
|
||||
onPaneStateChange?: (state: PaneState) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
message?: ApiMessage;
|
||||
sender?: ApiPeer;
|
||||
chat?: ApiChat;
|
||||
volume: number;
|
||||
@ -72,6 +79,8 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
playbackRate,
|
||||
isPlaybackRateActive,
|
||||
isMuted,
|
||||
isFullWidth,
|
||||
onPaneStateChange,
|
||||
}) => {
|
||||
const {
|
||||
setAudioPlayerVolume,
|
||||
@ -81,16 +90,19 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
closeAudioPlayer,
|
||||
} = getActions();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const lang = useOldLang();
|
||||
|
||||
const { isMobile } = useAppLayout();
|
||||
const { audio, voice, video } = getMessageContent(message);
|
||||
const renderingMessage = useCurrentOrPrev(message);
|
||||
|
||||
const { audio, voice, video } = renderingMessage ? getMessageContent(renderingMessage) : {} satisfies MediaContent;
|
||||
const isVoice = Boolean(voice || video);
|
||||
const shouldRenderPlaybackButton = isVoice || (audio?.duration || 0) > PLAYBACK_RATE_FOR_AUDIO_MIN_DURATION;
|
||||
const senderName = sender ? getSenderTitle(lang, sender) : undefined;
|
||||
const mediaData = mediaLoader.getFromMemory(getMessageMediaHash(message, 'inline')!) as (string | undefined);
|
||||
const mediaMetadata = useMessageMediaMetadata(message, sender, chat);
|
||||
|
||||
const mediaHash = renderingMessage && getMessageMediaHash(renderingMessage, 'inline');
|
||||
const mediaData = mediaHash && mediaLoader.getFromMemory(mediaHash);
|
||||
const mediaMetadata = useMessageMediaMetadata(renderingMessage, sender, chat);
|
||||
|
||||
const {
|
||||
playPause,
|
||||
@ -104,8 +116,8 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
toggleMuted,
|
||||
setPlaybackRate,
|
||||
} = useAudioPlayer(
|
||||
makeTrackId(message),
|
||||
getMediaDuration(message)!,
|
||||
message && makeTrackId(message),
|
||||
message ? getMediaDuration(message)! : 0,
|
||||
isVoice ? 'voice' : 'audio',
|
||||
mediaData,
|
||||
undefined,
|
||||
@ -114,18 +126,34 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
true,
|
||||
undefined,
|
||||
undefined,
|
||||
isMessageLocal(message),
|
||||
message && isMessageLocal(message),
|
||||
true,
|
||||
);
|
||||
|
||||
const isOpen = Boolean(message);
|
||||
const {
|
||||
ref: transitionRef,
|
||||
} = useShowTransition({
|
||||
isOpen,
|
||||
shouldForceOpen: isFullWidth, // Use pane animation instead
|
||||
});
|
||||
|
||||
const { ref, shouldRender } = useHeaderPane({
|
||||
isOpen,
|
||||
isDisabled: !isFullWidth,
|
||||
ref: transitionRef,
|
||||
onStateChange: onPaneStateChange,
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref);
|
||||
} = useContextMenuHandlers(transitionRef, !shouldRender);
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
const { chatId, id } = renderingMessage!;
|
||||
focusMessage({ chatId, messageId: id });
|
||||
});
|
||||
|
||||
const handleClose = useLastCallback(() => {
|
||||
@ -221,12 +249,16 @@ const AudioPlayer: FC<OwnProps & StateProps> = ({
|
||||
return 'icon-volume-3';
|
||||
}, [volume, isMuted]);
|
||||
|
||||
if (noUi) {
|
||||
if (noUi || !shouldRender) {
|
||||
return undefined;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName('AudioPlayer', className)} dir={lang.isRtl ? 'rtl' : undefined} ref={ref}>
|
||||
<div
|
||||
className={buildClassName('AudioPlayer', isFullWidth ? 'full-width-player' : 'mini-player', className)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
ref={ref}
|
||||
>
|
||||
<div className="AudioPlayer-content" onClick={handleClick}>
|
||||
{audio ? renderAudio(audio) : renderVoice(lang('AttachAudio'), senderName)}
|
||||
<RippleEffect />
|
||||
@ -365,14 +397,19 @@ function renderPlaybackRateMenuItem(
|
||||
}
|
||||
|
||||
export default withGlobal<OwnProps>(
|
||||
(global, { message }): StateProps => {
|
||||
const sender = selectSender(global, message);
|
||||
const chat = selectChat(global, message.chatId);
|
||||
(global, { isHidden }): StateProps => {
|
||||
const { audioPlayer } = selectTabState(global);
|
||||
const { chatId, messageId } = audioPlayer;
|
||||
const message = !isHidden && chatId && messageId ? selectChatMessage(global, chatId, messageId) : undefined;
|
||||
|
||||
const sender = message && selectSender(global, message);
|
||||
const chat = message && selectChat(global, message.chatId);
|
||||
const {
|
||||
volume, playbackRate, isMuted, isPlaybackRateActive,
|
||||
} = selectTabState(global).audioPlayer;
|
||||
|
||||
return {
|
||||
message,
|
||||
sender,
|
||||
chat,
|
||||
volume,
|
||||
20
src/components/middle/panes/ChatReportPane.scss
Normal file
20
src/components/middle/panes/ChatReportPane.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
@ -1,29 +1,36 @@
|
||||
import type { FC } from '../../lib/teact/teact';
|
||||
import React, { memo, useState } from '../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../global';
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useState } from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type { ApiChat, ApiChatSettings, ApiUser } from '../../api/types';
|
||||
import type { ApiChat, ApiUser } from '../../../api/types';
|
||||
|
||||
import {
|
||||
getChatTitle, getUserFirstOrLastName, getUserFullName, isChatBasicGroup,
|
||||
} from '../../global/helpers';
|
||||
import { selectChat, selectUser } from '../../global/selectors';
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
} from '../../../global/helpers';
|
||||
import { selectChat, selectUser } from '../../../global/selectors';
|
||||
import buildClassName from '../../../util/buildClassName';
|
||||
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useFlag from '../../../hooks/useFlag';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useHeaderPane, { type PaneState } from '../hooks/useHeaderPane';
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import Checkbox from '../ui/Checkbox';
|
||||
import ConfirmDialog from '../ui/ConfirmDialog';
|
||||
import Icon from '../../common/icons/Icon';
|
||||
import Button from '../../ui/Button';
|
||||
import Checkbox from '../../ui/Checkbox';
|
||||
import ConfirmDialog from '../../ui/ConfirmDialog';
|
||||
|
||||
import './ChatReportPanel.scss';
|
||||
import './ChatReportPane.scss';
|
||||
|
||||
type OwnProps = {
|
||||
chatId: string;
|
||||
className?: string;
|
||||
settings?: ApiChatSettings;
|
||||
isAutoArchived?: boolean;
|
||||
canReportSpam?: boolean;
|
||||
canAddContact?: boolean;
|
||||
canBlockContact?: boolean;
|
||||
onPaneStateChange?: (state: PaneState) => void;
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -32,8 +39,17 @@ type StateProps = {
|
||||
user?: ApiUser;
|
||||
};
|
||||
|
||||
const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
chatId, className, chat, user, settings, currentUserId,
|
||||
const ChatReportPane: FC<OwnProps & StateProps> = ({
|
||||
chatId,
|
||||
className,
|
||||
isAutoArchived,
|
||||
canReportSpam,
|
||||
canAddContact,
|
||||
canBlockContact,
|
||||
chat,
|
||||
user,
|
||||
currentUserId,
|
||||
onPaneStateChange,
|
||||
}) => {
|
||||
const {
|
||||
openAddContactDialog,
|
||||
@ -44,21 +60,23 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
deleteChatUser,
|
||||
deleteHistory,
|
||||
toggleChatArchived,
|
||||
hideChatReportPanel,
|
||||
hideChatReportPane,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
const [isBlockUserModalOpen, openBlockUserModal, closeBlockUserModal] = useFlag();
|
||||
const [shouldReportSpam, setShouldReportSpam] = useState<boolean>(true);
|
||||
const [shouldDeleteChat, setShouldDeleteChat] = useState<boolean>(true);
|
||||
const {
|
||||
isAutoArchived, canReportSpam, canAddContact, canBlockContact,
|
||||
} = settings || {};
|
||||
const isBasicGroup = chat && isChatBasicGroup(chat);
|
||||
|
||||
const renderingCanAddContact = useCurrentOrPrev(canAddContact);
|
||||
const renderingCanBlockContact = useCurrentOrPrev(canBlockContact);
|
||||
const renderingCanReportSpam = useCurrentOrPrev(canReportSpam);
|
||||
const renderingIsAutoArchived = useCurrentOrPrev(isAutoArchived);
|
||||
|
||||
const handleAddContact = useLastCallback(() => {
|
||||
openAddContactDialog({ userId: chatId });
|
||||
if (isAutoArchived) {
|
||||
if (renderingIsAutoArchived) {
|
||||
toggleChatArchived({ id: chatId });
|
||||
}
|
||||
});
|
||||
@ -66,7 +84,7 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
const handleConfirmBlock = useLastCallback(() => {
|
||||
closeBlockUserModal();
|
||||
blockUser({ userId: chatId });
|
||||
if (canReportSpam && shouldReportSpam) {
|
||||
if (renderingCanReportSpam && shouldReportSpam) {
|
||||
reportSpam({ chatId });
|
||||
}
|
||||
if (shouldDeleteChat) {
|
||||
@ -74,8 +92,8 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
const handleCloseReportPanel = useLastCallback(() => {
|
||||
hideChatReportPanel({ chatId });
|
||||
const handleCloseReportPane = useLastCallback(() => {
|
||||
hideChatReportPane({ chatId });
|
||||
});
|
||||
|
||||
const handleChatReportSpam = useLastCallback(() => {
|
||||
@ -89,42 +107,53 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
}
|
||||
});
|
||||
|
||||
if (!settings || (!chat && !user)) {
|
||||
return undefined;
|
||||
}
|
||||
const hasAnyButton = canAddContact || canBlockContact || canReportSpam;
|
||||
|
||||
const isRendering = Boolean(hasAnyButton && (chat || user));
|
||||
|
||||
const { ref, shouldRender } = useHeaderPane({
|
||||
isOpen: isRendering,
|
||||
onStateChange: onPaneStateChange,
|
||||
});
|
||||
|
||||
if (!shouldRender) return undefined;
|
||||
|
||||
return (
|
||||
<div className={buildClassName('ChatReportPanel', className)} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
{canAddContact && (
|
||||
<div
|
||||
ref={ref}
|
||||
className={buildClassName('ChatReportPane', className)}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
>
|
||||
{renderingCanAddContact && (
|
||||
<Button
|
||||
isText
|
||||
fluid
|
||||
size="tiny"
|
||||
className="UserReportPanel--Button"
|
||||
className="ChatReportPane--Button"
|
||||
onClick={handleAddContact}
|
||||
>
|
||||
{lang('lng_new_contact_add')}
|
||||
</Button>
|
||||
)}
|
||||
{canBlockContact && (
|
||||
{renderingCanBlockContact && (
|
||||
<Button
|
||||
color="danger"
|
||||
isText
|
||||
fluid
|
||||
size="tiny"
|
||||
className="UserReportPanel--Button"
|
||||
className="ChatReportPane--Button"
|
||||
onClick={openBlockUserModal}
|
||||
>
|
||||
{lang('lng_new_contact_block')}
|
||||
</Button>
|
||||
)}
|
||||
{canReportSpam && !canBlockContact && (
|
||||
{renderingCanReportSpam && !renderingCanBlockContact && (
|
||||
<Button
|
||||
color="danger"
|
||||
isText
|
||||
fluid
|
||||
size="tiny"
|
||||
className="UserReportPanel--Button"
|
||||
className="ChatReportPane--Button"
|
||||
onClick={openBlockUserModal}
|
||||
>
|
||||
{lang('lng_report_spam_and_leave')}
|
||||
@ -133,12 +162,12 @@ const ChatReportPanel: FC<OwnProps & StateProps> = ({
|
||||
<Button
|
||||
round
|
||||
ripple
|
||||
size="tiny"
|
||||
size="smaller"
|
||||
color="translucent"
|
||||
onClick={handleCloseReportPanel}
|
||||
onClick={handleCloseReportPane}
|
||||
ariaLabel={lang('Close')}
|
||||
>
|
||||
<i className="icon icon-close" />
|
||||
<Icon name="close" />
|
||||
</Button>
|
||||
<ConfirmDialog
|
||||
isOpen={isBlockUserModalOpen}
|
||||
@ -176,4 +205,4 @@ export default memo(withGlobal<OwnProps>(
|
||||
chat: selectChat(global, chatId),
|
||||
user: selectUser(global, chatId),
|
||||
}),
|
||||
)(ChatReportPanel));
|
||||
)(ChatReportPane));
|
||||
@ -1,4 +1,4 @@
|
||||
@use "../../styles/mixins";
|
||||
@use "../../../styles/mixins";
|
||||
|
||||
.root {
|
||||
display: flex;
|
||||
@ -26,6 +26,12 @@
|
||||
transition: none !important;
|
||||
}
|
||||
|
||||
> :global(.Button) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.mini {
|
||||
@media (min-width: 1276px) {
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: opacity 0.15s ease, transform var(--layer-transition);
|
||||
@ -38,33 +44,10 @@
|
||||
transform: translate3d(calc(var(--right-column-width) * -1), 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
> :global(.Button) {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.root:global(.full-width) {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 100%;
|
||||
background: var(--color-background);
|
||||
padding: 0.25rem 0.8125rem 0.25rem 1rem;
|
||||
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
||||
transform: translate3d(0, 0, 0);
|
||||
transition: opacity 0.15s ease, transform var(--layer-transition);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
display: block;
|
||||
position: absolute;
|
||||
top: -0.1875rem;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: 0.125rem;
|
||||
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
||||
}
|
||||
.fullWidth {
|
||||
@include mixins.header-pane;
|
||||
|
||||
.pinnedMessage {
|
||||
margin-top: 0;
|
||||
@ -75,12 +58,6 @@
|
||||
.messageText {
|
||||
max-width: none;
|
||||
}
|
||||
|
||||
@media (min-width: 1276px) {
|
||||
:global(#Main.right-column-open) & {
|
||||
padding-left: calc(var(--right-column-width) + 1rem);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.loading {
|
||||
@ -244,30 +221,4 @@
|
||||
max-width: none;
|
||||
}
|
||||
}
|
||||
|
||||
.root:global(.full-width) {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.root {
|
||||
@include mixins.header-mobile();
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width: 1276px) and (max-width: 1439px) {
|
||||
:global(.MiddleHeader:not(.tools-stacked)) .root {
|
||||
opacity: 1;
|
||||
|
||||
:global(#Main.right-column-open) & {
|
||||
opacity: 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
:global(.tools-stacked.animated) .root {
|
||||
animation: fade-in var(--layer-transition) forwards;
|
||||
|
||||
:global(body.no-page-transitions) & {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
395
src/components/middle/panes/HeaderPinnedMessage.tsx
Normal file
395
src/components/middle/panes/HeaderPinnedMessage.tsx
Normal 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));
|
||||
@ -3,22 +3,11 @@
|
||||
width: 22rem;
|
||||
max-width: 100vw;
|
||||
margin: 4.25rem auto 0.25rem;
|
||||
margin-top: calc(4.25rem + var(--middle-header-panes-height));
|
||||
z-index: var(--z-notification);
|
||||
|
||||
.has-header-tools & {
|
||||
margin-top: 7.375rem;
|
||||
}
|
||||
|
||||
@media (min-width: 1276px) {
|
||||
transition: transform var(--layer-transition);
|
||||
}
|
||||
|
||||
& ~ & {
|
||||
margin-top: 0.25rem;
|
||||
|
||||
.has-header-tools & {
|
||||
margin-top: 0.25rem;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -154,14 +154,8 @@ export const SNAP_EFFECT_ID = 'snap-effect';
|
||||
export const STARS_ICON_PLACEHOLDER = '⭐';
|
||||
export const STARS_CURRENCY_CODE = 'XTR';
|
||||
|
||||
// Screen width where Pinned Message / Audio Player in the Middle Header can be safely displayed
|
||||
export const SAFE_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN = 1440; // px
|
||||
// Screen width where Pinned Message / Audio Player in the Middle Header shouldn't collapse with ChatInfo
|
||||
export const SAFE_SCREEN_WIDTH_FOR_CHAT_INFO = 1150; // px
|
||||
|
||||
export const MIN_SCREEN_WIDTH_FOR_STATIC_RIGHT_COLUMN = 1275; // px
|
||||
export const MIN_SCREEN_WIDTH_FOR_STATIC_LEFT_COLUMN = 925; // px
|
||||
export const MAX_SCREEN_WIDTH_FOR_EXPAND_PINNED_MESSAGES = 1340; // px
|
||||
export const MOBILE_SCREEN_MAX_WIDTH = 600; // px
|
||||
export const MOBILE_SCREEN_LANDSCAPE_MAX_WIDTH = 950; // px
|
||||
export const MOBILE_SCREEN_LANDSCAPE_MAX_HEIGHT = 450; // px
|
||||
|
||||
@ -394,12 +394,12 @@ addActionHandler('hideAllChatJoinRequests', async (global, actions, payload): Pr
|
||||
setGlobal(global);
|
||||
});
|
||||
|
||||
addActionHandler('hideChatReportPanel', async (global, actions, payload): Promise<void> => {
|
||||
addActionHandler('hideChatReportPane', async (global, actions, payload): Promise<void> => {
|
||||
const { chatId } = payload!;
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) return;
|
||||
|
||||
const result = await callApi('hideChatReportPanel', chat);
|
||||
const result = await callApi('hideChatReportPane', chat);
|
||||
if (!result) return;
|
||||
|
||||
global = getGlobal();
|
||||
|
||||
@ -696,10 +696,10 @@ addActionHandler('toggleMessageWebPage', (global, actions, payload): ActionRetur
|
||||
|
||||
addActionHandler('pinMessage', (global, actions, payload): ActionReturnType => {
|
||||
const {
|
||||
messageId, isUnpin, isOneSide, isSilent, tabId = getCurrentTabId(),
|
||||
chatId, messageId, isUnpin, isOneSide, isSilent,
|
||||
} = payload;
|
||||
|
||||
const chat = selectCurrentChat(global, tabId);
|
||||
const chat = selectChat(global, chatId);
|
||||
if (!chat) {
|
||||
return;
|
||||
}
|
||||
|
||||
@ -13,6 +13,7 @@ import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type {
|
||||
CustomPeer, NotifyException, NotifySettings, ThreadId,
|
||||
} from '../../types';
|
||||
import type { LangFn } from '../../util/localization';
|
||||
import { MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
import {
|
||||
@ -101,7 +102,7 @@ export function getPrivateChatUserId(chat: ApiChat) {
|
||||
return chat.id;
|
||||
}
|
||||
|
||||
export function getChatTitle(lang: OldLangFn, chat: ApiChat, isSelf = false) {
|
||||
export function getChatTitle(lang: OldLangFn | LangFn, chat: ApiChat, isSelf = false) {
|
||||
if (isSelf) {
|
||||
return lang('SavedMessages');
|
||||
}
|
||||
|
||||
@ -11,6 +11,7 @@ import type {
|
||||
} from '../../api/types/messages';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { ThreadId } from '../../types';
|
||||
import type { LangFn } from '../../util/localization';
|
||||
import type { GlobalState } from '../types';
|
||||
import { ApiMessageEntityTypes, MAIN_THREAD_ID } from '../../api/types';
|
||||
|
||||
@ -208,7 +209,7 @@ export function isAnonymousOwnMessage(message: ApiMessage) {
|
||||
return Boolean(message.senderId) && !isUserId(message.senderId) && isOwnMessage(message);
|
||||
}
|
||||
|
||||
export function getSenderTitle(lang: OldLangFn, sender: ApiPeer) {
|
||||
export function getSenderTitle(lang: OldLangFn | LangFn, sender: ApiPeer) {
|
||||
return isPeerUser(sender) ? getUserFullName(sender) : getChatTitle(lang, sender);
|
||||
}
|
||||
|
||||
|
||||
@ -1695,11 +1695,12 @@ export interface ActionPayloads {
|
||||
messageId: number;
|
||||
};
|
||||
pinMessage: {
|
||||
chatId: string;
|
||||
messageId: number;
|
||||
isUnpin: boolean;
|
||||
isOneSide?: boolean;
|
||||
isSilent?: boolean;
|
||||
} & WithTabId;
|
||||
};
|
||||
deleteMessages: {
|
||||
messageIds: number[];
|
||||
shouldDeleteForAll?: boolean;
|
||||
@ -2114,7 +2115,7 @@ export interface ActionPayloads {
|
||||
offsetUserId?: string;
|
||||
limit?: number;
|
||||
} & WithTabId;
|
||||
hideChatReportPanel: {
|
||||
hideChatReportPane: {
|
||||
chatId: string;
|
||||
};
|
||||
toggleManagement: ({
|
||||
|
||||
@ -2,6 +2,7 @@ import { useMemo } from '../lib/teact/teact';
|
||||
|
||||
import type {
|
||||
ApiAudio, ApiChat, ApiMessage, ApiPeer, ApiVoice,
|
||||
MediaContent,
|
||||
} from '../api/types';
|
||||
|
||||
import {
|
||||
@ -21,11 +22,11 @@ const MINIMAL_SIZE = 115; // spec says 100, but on Chrome 93 it's not showing
|
||||
|
||||
// TODO Add support for video in future
|
||||
const useMessageMediaMetadata = (
|
||||
message: ApiMessage, sender?: ApiPeer, chat?: ApiChat,
|
||||
message?: ApiMessage, sender?: ApiPeer, chat?: ApiChat,
|
||||
): MediaMetadata | undefined => {
|
||||
const lang = useOldLang();
|
||||
|
||||
const { audio, voice } = getMessageContent(message);
|
||||
const { audio, voice } = message ? getMessageContent(message) : {} satisfies MediaContent;
|
||||
const title = audio ? (audio.title || audio.fileName) : voice ? 'Voice message' : '';
|
||||
const artist = audio?.performer || (sender && getSenderTitle(lang, sender));
|
||||
const album = (chat && getChatTitle(lang, chat)) || 'Telegram';
|
||||
|
||||
@ -23,6 +23,7 @@ type BaseHookParams<RefType extends HTMLElement> = {
|
||||
closeDuration?: number;
|
||||
className?: string | false;
|
||||
prefix?: string;
|
||||
shouldForceOpen?: boolean;
|
||||
onCloseAnimationEnd?: NoneToVoidFunction;
|
||||
};
|
||||
|
||||
@ -66,6 +67,7 @@ export default function useShowTransition<RefType extends HTMLElement = HTMLDivE
|
||||
closeDuration = CLOSE_DURATION,
|
||||
className = 'fast',
|
||||
prefix = '',
|
||||
shouldForceOpen,
|
||||
onCloseAnimationEnd,
|
||||
} = params;
|
||||
|
||||
@ -82,6 +84,11 @@ export default function useShowTransition<RefType extends HTMLElement = HTMLDivE
|
||||
useSyncEffectWithPrevDeps(([prevIsOpen]) => {
|
||||
const options = optionsRef.current;
|
||||
|
||||
if (shouldForceOpen) {
|
||||
setState('open');
|
||||
return;
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
if (closingTimeoutRef.current) {
|
||||
clearTimeout(closingTimeoutRef.current);
|
||||
@ -106,7 +113,7 @@ export default function useShowTransition<RefType extends HTMLElement = HTMLDivE
|
||||
onCloseEndLast();
|
||||
}, options.closeDuration);
|
||||
}
|
||||
}, [isOpen]);
|
||||
}, [isOpen, shouldForceOpen]);
|
||||
|
||||
const applyClassNames = useLastCallback(() => {
|
||||
const element = ref.current;
|
||||
|
||||
@ -117,21 +117,19 @@
|
||||
}
|
||||
}
|
||||
|
||||
@mixin header-mobile {
|
||||
@mixin header-pane {
|
||||
position: absolute;
|
||||
top: 100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
top: 0;
|
||||
width: 100%;
|
||||
height: 2.875rem;
|
||||
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
||||
transform: translateY(-100%);
|
||||
transition: transform var(--slide-transition);
|
||||
|
||||
display: flex;
|
||||
flex-direction: row-reverse;
|
||||
padding-top: 0.375rem;
|
||||
padding-bottom: 0.375rem;
|
||||
padding-left: max(0.75rem, env(safe-area-inset-left));
|
||||
padding-right: max(0.5rem, env(safe-area-inset-right));
|
||||
background: var(--color-background);
|
||||
background-color: var(--color-background);
|
||||
|
||||
&::before {
|
||||
content: "";
|
||||
@ -143,6 +141,18 @@
|
||||
height: 0.125rem;
|
||||
box-shadow: 0 0.125rem 0.125rem var(--color-light-shadow);
|
||||
}
|
||||
|
||||
// Some panels might unmount without animation, so we provide same background above panel to make it less noticeable
|
||||
&::after {
|
||||
content: "";
|
||||
position: absolute;
|
||||
top: -100%;
|
||||
left: 0;
|
||||
right: 0;
|
||||
height: inherit;
|
||||
background-color: inherit;
|
||||
z-index: -1;
|
||||
}
|
||||
}
|
||||
|
||||
@mixin side-panel-section {
|
||||
|
||||
7
src/types/language.d.ts
vendored
7
src/types/language.d.ts
vendored
@ -574,7 +574,9 @@ export interface LangPair {
|
||||
'Statistics': undefined;
|
||||
'EventLogFilterPinnedMessages': undefined;
|
||||
'UnpinMessageAlertTitle': undefined;
|
||||
'PinnedMessage': undefined;
|
||||
'PinnedMessageTitleSingle': undefined;
|
||||
'AccPinnedMessages': undefined;
|
||||
'AccUnpinMessage': undefined;
|
||||
'LeaveAComment': undefined;
|
||||
'PollsStopWarning': undefined;
|
||||
'PollsStopSure': undefined;
|
||||
@ -1589,6 +1591,9 @@ export interface LangPairPluralWithVariables<V extends unknown = LangVariable> {
|
||||
'PreviewSenderSendFile': {
|
||||
'count': V;
|
||||
};
|
||||
'PinnedMessageTitle': {
|
||||
'index': V;
|
||||
};
|
||||
'Comments': {
|
||||
'count': V;
|
||||
};
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user