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