Introduce Quick Preview (#6298)
This commit is contained in:
parent
729ed3dd7d
commit
2b7fcbb240
@ -2281,3 +2281,4 @@
|
|||||||
"GiftValueForSaleOnFragment" = "for sale on Fragment";
|
"GiftValueForSaleOnFragment" = "for sale on Fragment";
|
||||||
"GiftValueForSaleOnTelegram" = "for sale on Telegram";
|
"GiftValueForSaleOnTelegram" = "for sale on Telegram";
|
||||||
"EmbeddedMessageNoCaption" = "Caption removed";
|
"EmbeddedMessageNoCaption" = "Caption removed";
|
||||||
|
"QuickPreview" = "Quick Preview";
|
||||||
|
|||||||
@ -104,3 +104,4 @@ export { default as OneTimeMediaModal } from '../components/modals/oneTimeMedia/
|
|||||||
export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal';
|
export { default as WebAppsCloseConfirmationModal } from '../components/main/WebAppsCloseConfirmationModal';
|
||||||
export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal';
|
export { default as FrozenAccountModal } from '../components/modals/frozenAccount/FrozenAccountModal';
|
||||||
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';
|
export { default as ProfileRatingModal } from '../components/modals/profileRating/ProfileRatingModal';
|
||||||
|
export { default as QuickPreviewModal } from '../components/modals/quickPreview/QuickPreviewModal';
|
||||||
|
|||||||
@ -186,6 +186,7 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
reportMessages,
|
reportMessages,
|
||||||
openFrozenAccountModal,
|
openFrozenAccountModal,
|
||||||
updateChatMutedState,
|
updateChatMutedState,
|
||||||
|
openQuickPreview,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const { isMobile } = useAppLayout();
|
const { isMobile } = useAppLayout();
|
||||||
@ -239,7 +240,13 @@ const Chat: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
|
const getIsForumPanelClosed = useSelectorSignal(selectIsForumPanelClosed);
|
||||||
|
|
||||||
const handleClick = useLastCallback(() => {
|
const handleClick = useLastCallback((e: React.MouseEvent) => {
|
||||||
|
if (e.altKey && !isSavedDialog && !isForum && !isPreview) {
|
||||||
|
e.preventDefault();
|
||||||
|
openQuickPreview({ id: chatId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const noForumTopicPanel = isMobile && isForumAsMessages;
|
const noForumTopicPanel = isMobile && isForumAsMessages;
|
||||||
|
|
||||||
if (isMobile) {
|
if (isMobile) {
|
||||||
|
|||||||
@ -103,6 +103,7 @@ const Topic: FC<OwnProps & StateProps> = ({
|
|||||||
focusLastMessage,
|
focusLastMessage,
|
||||||
setViewForumAsMessages,
|
setViewForumAsMessages,
|
||||||
updateTopicMutedState,
|
updateTopicMutedState,
|
||||||
|
openQuickPreview,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const lang = useOldLang();
|
const lang = useOldLang();
|
||||||
@ -153,7 +154,13 @@ const Topic: FC<OwnProps & StateProps> = ({
|
|||||||
orderDiff,
|
orderDiff,
|
||||||
});
|
});
|
||||||
|
|
||||||
const handleOpenTopic = useLastCallback(() => {
|
const handleOpenTopic = useLastCallback((e: React.MouseEvent) => {
|
||||||
|
if (e.altKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
openQuickPreview({ id: chatId, threadId: topic.id });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true });
|
openThread({ chatId, threadId: topic.id, shouldReplaceHistory: true });
|
||||||
setViewForumAsMessages({ chatId, isEnabled: false });
|
setViewForumAsMessages({ chatId, isEnabled: false });
|
||||||
|
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import { getCanManageTopic, getHasAdminRight } from '../../../../global/helpers'
|
|||||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment';
|
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../../../../util/browser/windowEnvironment';
|
||||||
import { compact } from '../../../../util/iteratees';
|
import { compact } from '../../../../util/iteratees';
|
||||||
|
|
||||||
|
import useLang from '../../../../hooks/useLang';
|
||||||
import useOldLang from '../../../../hooks/useOldLang';
|
import useOldLang from '../../../../hooks/useOldLang';
|
||||||
|
|
||||||
export default function useTopicContextActions({
|
export default function useTopicContextActions({
|
||||||
@ -29,7 +30,8 @@ export default function useTopicContextActions({
|
|||||||
handleMute?: NoneToVoidFunction;
|
handleMute?: NoneToVoidFunction;
|
||||||
handleUnmute?: NoneToVoidFunction;
|
handleUnmute?: NoneToVoidFunction;
|
||||||
}) {
|
}) {
|
||||||
const lang = useOldLang();
|
const lang = useLang();
|
||||||
|
const oldLang = useOldLang();
|
||||||
|
|
||||||
return useMemo(() => {
|
return useMemo(() => {
|
||||||
const {
|
const {
|
||||||
@ -43,6 +45,7 @@ export default function useTopicContextActions({
|
|||||||
toggleTopicPinned,
|
toggleTopicPinned,
|
||||||
markTopicRead,
|
markTopicRead,
|
||||||
openChatInNewTab,
|
openChatInNewTab,
|
||||||
|
openQuickPreview,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const canToggleClosed = getCanManageTopic(chat, topic);
|
const canToggleClosed = getCanManageTopic(chat, topic);
|
||||||
@ -56,9 +59,17 @@ export default function useTopicContextActions({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actionQuickPreview = {
|
||||||
|
title: lang('QuickPreview'),
|
||||||
|
icon: 'eye-outline',
|
||||||
|
handler: () => {
|
||||||
|
openQuickPreview({ id: chatId, threadId: topicId });
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const actionUnreadMark = topic.unreadCount || !wasOpened
|
const actionUnreadMark = topic.unreadCount || !wasOpened
|
||||||
? {
|
? {
|
||||||
title: lang('MarkAsRead'),
|
title: oldLang('MarkAsRead'),
|
||||||
icon: 'readchats',
|
icon: 'readchats',
|
||||||
handler: () => {
|
handler: () => {
|
||||||
markTopicRead({ chatId, topicId });
|
markTopicRead({ chatId, topicId });
|
||||||
@ -68,42 +79,42 @@ export default function useTopicContextActions({
|
|||||||
|
|
||||||
const actionPin = canTogglePinned ? (isPinned
|
const actionPin = canTogglePinned ? (isPinned
|
||||||
? {
|
? {
|
||||||
title: lang('UnpinFromTop'),
|
title: oldLang('UnpinFromTop'),
|
||||||
icon: 'unpin',
|
icon: 'unpin',
|
||||||
handler: () => toggleTopicPinned({ chatId, topicId, isPinned: false }),
|
handler: () => toggleTopicPinned({ chatId, topicId, isPinned: false }),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: lang('PinToTop'),
|
title: oldLang('PinToTop'),
|
||||||
icon: 'pin',
|
icon: 'pin',
|
||||||
handler: () => toggleTopicPinned({ chatId, topicId, isPinned: true }),
|
handler: () => toggleTopicPinned({ chatId, topicId, isPinned: true }),
|
||||||
}) : undefined;
|
}) : undefined;
|
||||||
|
|
||||||
const actionMute = ((isChatMuted && notifySettings.mutedUntil === undefined) || notifySettings.mutedUntil)
|
const actionMute = ((isChatMuted && notifySettings.mutedUntil === undefined) || notifySettings.mutedUntil)
|
||||||
? {
|
? {
|
||||||
title: lang('ChatList.Unmute'),
|
title: oldLang('ChatList.Unmute'),
|
||||||
icon: 'unmute',
|
icon: 'unmute',
|
||||||
handler: handleUnmute,
|
handler: handleUnmute,
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: `${lang('ChatList.Mute')}...`,
|
title: `${oldLang('ChatList.Mute')}...`,
|
||||||
icon: 'mute',
|
icon: 'mute',
|
||||||
handler: handleMute,
|
handler: handleMute,
|
||||||
};
|
};
|
||||||
|
|
||||||
const actionCloseTopic = canToggleClosed ? (isClosed
|
const actionCloseTopic = canToggleClosed ? (isClosed
|
||||||
? {
|
? {
|
||||||
title: lang('lng_forum_topic_reopen'),
|
title: oldLang('lng_forum_topic_reopen'),
|
||||||
icon: 'reopen-topic',
|
icon: 'reopen-topic',
|
||||||
handler: () => editTopic({ chatId, topicId, isClosed: false }),
|
handler: () => editTopic({ chatId, topicId, isClosed: false }),
|
||||||
}
|
}
|
||||||
: {
|
: {
|
||||||
title: lang('lng_forum_topic_close'),
|
title: oldLang('lng_forum_topic_close'),
|
||||||
icon: 'close-topic',
|
icon: 'close-topic',
|
||||||
handler: () => editTopic({ chatId, topicId, isClosed: true }),
|
handler: () => editTopic({ chatId, topicId, isClosed: true }),
|
||||||
}) : undefined;
|
}) : undefined;
|
||||||
|
|
||||||
const actionDelete = canDelete ? {
|
const actionDelete = canDelete ? {
|
||||||
title: lang('lng_forum_topic_delete'),
|
title: oldLang('lng_forum_topic_delete'),
|
||||||
icon: 'delete',
|
icon: 'delete',
|
||||||
destructive: true,
|
destructive: true,
|
||||||
handler: handleDelete,
|
handler: handleDelete,
|
||||||
@ -111,11 +122,12 @@ export default function useTopicContextActions({
|
|||||||
|
|
||||||
return compact([
|
return compact([
|
||||||
actionOpenInNewTab,
|
actionOpenInNewTab,
|
||||||
|
actionQuickPreview,
|
||||||
actionPin,
|
actionPin,
|
||||||
actionUnreadMark,
|
actionUnreadMark,
|
||||||
actionMute,
|
actionMute,
|
||||||
actionCloseTopic,
|
actionCloseTopic,
|
||||||
actionDelete,
|
actionDelete,
|
||||||
]) as MenuItemContextAction[];
|
]) as MenuItemContextAction[];
|
||||||
}, [topic, chat, isChatMuted, wasOpened, lang, canDelete, handleDelete, handleMute, handleUnmute]);
|
}, [topic, chat, isChatMuted, wasOpened, lang, oldLang, canDelete, handleDelete, handleMute, handleUnmute]);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -52,7 +52,7 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
|||||||
withOpenAppButton,
|
withOpenAppButton,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const { requestMainWebView, updateChatMutedState } = getActions();
|
const { requestMainWebView, updateChatMutedState, openQuickPreview } = getActions();
|
||||||
const oldLang = useOldLang();
|
const oldLang = useOldLang();
|
||||||
|
|
||||||
const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag();
|
const [isMuteModalOpen, openMuteModal, closeMuteModal] = useFlag();
|
||||||
@ -85,7 +85,12 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
|||||||
handleChatFolderChange,
|
handleChatFolderChange,
|
||||||
}, true);
|
}, true);
|
||||||
|
|
||||||
const handleClick = useLastCallback(() => {
|
const handleClick = useLastCallback((e: React.MouseEvent) => {
|
||||||
|
if (e.altKey && chat && !chat.isForum) {
|
||||||
|
e.preventDefault();
|
||||||
|
openQuickPreview({ id: chatId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
onClick(chatId);
|
onClick(chatId);
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -100,7 +105,9 @@ const LeftSearchResultChat: FC<OwnProps & StateProps> = ({
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
const buttonRef = useSelectWithEnter(handleClick);
|
const buttonRef = useSelectWithEnter(() => {
|
||||||
|
onClick(chatId);
|
||||||
|
});
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ListItem
|
<ListItem
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import type { FC } from '../../../lib/teact/teact';
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
import { memo, useCallback } from '../../../lib/teact/teact';
|
import { memo, useCallback } from '../../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../../global';
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
import type { ApiTopic } from '../../../api/types';
|
import type { ApiTopic } from '../../../api/types';
|
||||||
|
|
||||||
@ -26,15 +26,23 @@ type StateProps = {
|
|||||||
const TOPIC_ICON_SIZE = 2 * REM;
|
const TOPIC_ICON_SIZE = 2 * REM;
|
||||||
|
|
||||||
const LeftSearchResultTopic: FC<OwnProps & StateProps> = ({
|
const LeftSearchResultTopic: FC<OwnProps & StateProps> = ({
|
||||||
|
chatId,
|
||||||
topicId,
|
topicId,
|
||||||
topic,
|
topic,
|
||||||
onClick,
|
onClick,
|
||||||
}) => {
|
}) => {
|
||||||
const handleClick = useCallback(() => {
|
const { openQuickPreview } = getActions();
|
||||||
onClick(topicId);
|
|
||||||
}, [topicId, onClick]);
|
|
||||||
|
|
||||||
const buttonRef = useSelectWithEnter(handleClick);
|
const handleClick = useCallback((e: React.MouseEvent) => {
|
||||||
|
if (e.altKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
openQuickPreview({ id: chatId, threadId: topicId });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
onClick(topicId);
|
||||||
|
}, [chatId, topicId, onClick, openQuickPreview]);
|
||||||
|
|
||||||
|
const buttonRef = useSelectWithEnter(() => onClick(topicId));
|
||||||
|
|
||||||
if (!topic) {
|
if (!topic) {
|
||||||
return undefined;
|
return undefined;
|
||||||
|
|||||||
@ -96,9 +96,10 @@ type OwnProps = {
|
|||||||
withDefaultBg: boolean;
|
withDefaultBg: boolean;
|
||||||
isContactRequirePremium?: boolean;
|
isContactRequirePremium?: boolean;
|
||||||
paidMessagesStars?: number;
|
paidMessagesStars?: number;
|
||||||
onScrollDownToggle: BooleanToVoidFunction;
|
isQuickPreview?: boolean;
|
||||||
onNotchToggle: BooleanToVoidFunction;
|
onScrollDownToggle?: BooleanToVoidFunction;
|
||||||
onIntersectPinnedMessage: OnIntersectPinnedMessage;
|
onNotchToggle?: AnyToVoidFunction;
|
||||||
|
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
||||||
};
|
};
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
@ -224,6 +225,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
canTranslate,
|
canTranslate,
|
||||||
translationLanguage,
|
translationLanguage,
|
||||||
shouldAutoTranslate,
|
shouldAutoTranslate,
|
||||||
|
isQuickPreview,
|
||||||
onIntersectPinnedMessage,
|
onIntersectPinnedMessage,
|
||||||
onScrollDownToggle,
|
onScrollDownToggle,
|
||||||
onNotchToggle,
|
onNotchToggle,
|
||||||
@ -443,7 +445,12 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
return debounce(() => loadViewportMessages({ direction: LoadMoreDirection.Around }), 1000, true, false);
|
return debounce(
|
||||||
|
() => loadViewportMessages({ direction: LoadMoreDirection.Around, chatId, threadId }),
|
||||||
|
1000,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
);
|
||||||
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
// eslint-disable-next-line react-hooks-static-deps/exhaustive-deps
|
||||||
}, [loadViewportMessages, messageIds]);
|
}, [loadViewportMessages, messageIds]);
|
||||||
|
|
||||||
@ -469,7 +476,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
const isFocusing = Boolean(selectTabState(global).focusedMessage?.chatId);
|
const isFocusing = Boolean(selectTabState(global).focusedMessage?.chatId);
|
||||||
if (!isFocusing) {
|
if (!isFocusing) {
|
||||||
onIntersectPinnedMessage({ shouldCancelWaiting: true });
|
onIntersectPinnedMessage?.({ shouldCancelWaiting: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!container.parentElement) {
|
if (!container.parentElement) {
|
||||||
@ -478,7 +485,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
|
|
||||||
scrollOffsetRef.current = container.scrollHeight - container.scrollTop;
|
scrollOffsetRef.current = container.scrollHeight - container.scrollTop;
|
||||||
|
|
||||||
if (type === 'thread') {
|
if (type === 'thread' && !isQuickPreview) {
|
||||||
setScrollOffset({ chatId, threadId, scrollOffset: scrollOffsetRef.current });
|
setScrollOffset({ chatId, threadId, scrollOffset: scrollOffsetRef.current });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@ -717,7 +724,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (hasMessages) return;
|
if (hasMessages) return;
|
||||||
|
|
||||||
onScrollDownToggle(false);
|
onScrollDownToggle?.(false);
|
||||||
}, [hasMessages, onScrollDownToggle]);
|
}, [hasMessages, onScrollDownToggle]);
|
||||||
|
|
||||||
const activeKey = isRestricted ? (
|
const activeKey = isRestricted ? (
|
||||||
@ -790,6 +797,7 @@ const MessageList: FC<OwnProps & StateProps> = ({
|
|||||||
nameChangeDate={nameChangeDate}
|
nameChangeDate={nameChangeDate}
|
||||||
photoChangeDate={photoChangeDate}
|
photoChangeDate={photoChangeDate}
|
||||||
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
|
noAppearanceAnimation={!messageGroups || !shouldAnimateAppearanceRef.current}
|
||||||
|
isQuickPreview={isQuickPreview}
|
||||||
onScrollDownToggle={onScrollDownToggle}
|
onScrollDownToggle={onScrollDownToggle}
|
||||||
onNotchToggle={onNotchToggle}
|
onNotchToggle={onNotchToggle}
|
||||||
onIntersectPinnedMessage={onIntersectPinnedMessage}
|
onIntersectPinnedMessage={onIntersectPinnedMessage}
|
||||||
|
|||||||
@ -74,9 +74,10 @@ interface OwnProps {
|
|||||||
photoChangeDate?: number;
|
photoChangeDate?: number;
|
||||||
noAppearanceAnimation: boolean;
|
noAppearanceAnimation: boolean;
|
||||||
isSavedDialog?: boolean;
|
isSavedDialog?: boolean;
|
||||||
onScrollDownToggle: BooleanToVoidFunction;
|
isQuickPreview?: boolean;
|
||||||
onNotchToggle: AnyToVoidFunction;
|
onScrollDownToggle?: BooleanToVoidFunction;
|
||||||
onIntersectPinnedMessage: OnIntersectPinnedMessage;
|
onNotchToggle?: AnyToVoidFunction;
|
||||||
|
onIntersectPinnedMessage?: OnIntersectPinnedMessage;
|
||||||
canPost?: boolean;
|
canPost?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -110,6 +111,7 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
photoChangeDate,
|
photoChangeDate,
|
||||||
noAppearanceAnimation,
|
noAppearanceAnimation,
|
||||||
isSavedDialog,
|
isSavedDialog,
|
||||||
|
isQuickPreview,
|
||||||
onScrollDownToggle,
|
onScrollDownToggle,
|
||||||
onNotchToggle,
|
onNotchToggle,
|
||||||
onIntersectPinnedMessage,
|
onIntersectPinnedMessage,
|
||||||
@ -126,7 +128,7 @@ const MessageListContent: FC<OwnProps> = ({
|
|||||||
observeIntersectionForReading,
|
observeIntersectionForReading,
|
||||||
observeIntersectionForLoading,
|
observeIntersectionForLoading,
|
||||||
observeIntersectionForPlaying,
|
observeIntersectionForPlaying,
|
||||||
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onIntersectPinnedMessage, chatId);
|
} = useMessageObservers(type, containerRef, memoFirstUnreadIdRef, onIntersectPinnedMessage, chatId, isQuickPreview);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
withHistoryTriggers,
|
withHistoryTriggers,
|
||||||
|
|||||||
@ -102,7 +102,7 @@ import ReactorListModal from './ReactorListModal.async';
|
|||||||
import MiddleSearch from './search/MiddleSearch.async';
|
import MiddleSearch from './search/MiddleSearch.async';
|
||||||
|
|
||||||
import './MiddleColumn.scss';
|
import './MiddleColumn.scss';
|
||||||
import styles from './MiddleColumn.module.scss';
|
import backgroundStyles from '../../styles/_patternBackground.module.scss';
|
||||||
|
|
||||||
interface OwnProps {
|
interface OwnProps {
|
||||||
leftColumnRef: ElementRef<HTMLDivElement>;
|
leftColumnRef: ElementRef<HTMLDivElement>;
|
||||||
@ -439,12 +439,12 @@ function MiddleColumn({
|
|||||||
);
|
);
|
||||||
|
|
||||||
const bgClassName = buildClassName(
|
const bgClassName = buildClassName(
|
||||||
styles.background,
|
backgroundStyles.background,
|
||||||
withRightColumnAnimation && styles.withTransition,
|
withRightColumnAnimation && backgroundStyles.withTransition,
|
||||||
customBackground && styles.customBgImage,
|
customBackground && backgroundStyles.customBgImage,
|
||||||
backgroundColor && styles.customBgColor,
|
backgroundColor && backgroundStyles.customBgColor,
|
||||||
customBackground && isBackgroundBlurred && styles.blurred,
|
customBackground && isBackgroundBlurred && backgroundStyles.blurred,
|
||||||
isRightColumnShown && styles.withRightColumn,
|
isRightColumnShown && backgroundStyles.withRightColumn,
|
||||||
);
|
);
|
||||||
|
|
||||||
const messagingDisabledClassName = buildClassName(
|
const messagingDisabledClassName = buildClassName(
|
||||||
@ -578,7 +578,7 @@ function MiddleColumn({
|
|||||||
paidMessagesStars={paidMessagesStars}
|
paidMessagesStars={paidMessagesStars}
|
||||||
withBottomShift={withMessageListBottomShift}
|
withBottomShift={withMessageListBottomShift}
|
||||||
withDefaultBg={Boolean(!customBackground && !backgroundColor)}
|
withDefaultBg={Boolean(!customBackground && !backgroundColor)}
|
||||||
onIntersectPinnedMessage={renderingHandleIntersectPinnedMessage!}
|
onIntersectPinnedMessage={renderingHandleIntersectPinnedMessage}
|
||||||
/>
|
/>
|
||||||
<div className={footerClassName}>
|
<div className={footerClassName}>
|
||||||
<FloatingActionButtons
|
<FloatingActionButtons
|
||||||
|
|||||||
@ -17,8 +17,9 @@ export default function useMessageObservers(
|
|||||||
type: MessageListType,
|
type: MessageListType,
|
||||||
containerRef: ElementRef<HTMLDivElement>,
|
containerRef: ElementRef<HTMLDivElement>,
|
||||||
memoFirstUnreadIdRef: { current: number | undefined },
|
memoFirstUnreadIdRef: { current: number | undefined },
|
||||||
onIntersectPinnedMessage: OnIntersectPinnedMessage,
|
onIntersectPinnedMessage: OnIntersectPinnedMessage | undefined,
|
||||||
chatId: string,
|
chatId: string,
|
||||||
|
isQuickPreview?: boolean,
|
||||||
) {
|
) {
|
||||||
const {
|
const {
|
||||||
markMessageListRead, markMentionsRead, animateUnreadReaction,
|
markMessageListRead, markMentionsRead, animateUnreadReaction,
|
||||||
@ -81,12 +82,18 @@ export default function useMessageObservers(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
|
if (!isQuickPreview) {
|
||||||
markMessageListRead({ maxId });
|
if (memoFirstUnreadIdRef.current && maxId >= memoFirstUnreadIdRef.current) {
|
||||||
}
|
markMessageListRead({ maxId });
|
||||||
|
}
|
||||||
|
|
||||||
if (mentionIds.length) {
|
if (mentionIds.length) {
|
||||||
markMentionsRead({ chatId, messageIds: mentionIds });
|
markMentionsRead({ chatId, messageIds: mentionIds });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (scheduledToUpdateViews.length) {
|
||||||
|
scheduleForViewsIncrement({ chatId, ids: scheduledToUpdateViews });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (reactionIds.length) {
|
if (reactionIds.length) {
|
||||||
@ -94,11 +101,7 @@ export default function useMessageObservers(
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (viewportPinnedIdsToAdd.length || viewportPinnedIdsToRemove.length) {
|
if (viewportPinnedIdsToAdd.length || viewportPinnedIdsToRemove.length) {
|
||||||
onIntersectPinnedMessage({ viewportPinnedIdsToAdd, viewportPinnedIdsToRemove });
|
onIntersectPinnedMessage?.({ viewportPinnedIdsToAdd, viewportPinnedIdsToRemove });
|
||||||
}
|
|
||||||
|
|
||||||
if (scheduledToUpdateViews.length) {
|
|
||||||
scheduleForViewsIncrement({ chatId, ids: scheduledToUpdateViews });
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -28,8 +28,8 @@ export default function useScrollHooks(
|
|||||||
getContainerHeight: Signal<number | undefined>,
|
getContainerHeight: Signal<number | undefined>,
|
||||||
isViewportNewest: boolean,
|
isViewportNewest: boolean,
|
||||||
isUnread: boolean,
|
isUnread: boolean,
|
||||||
onScrollDownToggle: BooleanToVoidFunction,
|
onScrollDownToggle: BooleanToVoidFunction | undefined,
|
||||||
onNotchToggle: BooleanToVoidFunction,
|
onNotchToggle: AnyToVoidFunction | undefined,
|
||||||
isReady: boolean,
|
isReady: boolean,
|
||||||
) {
|
) {
|
||||||
const { loadViewportMessages } = getActions();
|
const { loadViewportMessages } = getActions();
|
||||||
@ -51,14 +51,16 @@ export default function useScrollHooks(
|
|||||||
if (!isReady) return;
|
if (!isReady) return;
|
||||||
|
|
||||||
if (!messageIds?.length) {
|
if (!messageIds?.length) {
|
||||||
onScrollDownToggle(false);
|
onScrollDownToggle?.(false);
|
||||||
onNotchToggle(false);
|
onNotchToggle?.(false);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!isViewportNewest) {
|
if (!isViewportNewest) {
|
||||||
onScrollDownToggle(true);
|
onScrollDownToggle?.(true);
|
||||||
onNotchToggle(true);
|
onNotchToggle?.(true);
|
||||||
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -74,8 +76,8 @@ export default function useScrollHooks(
|
|||||||
|
|
||||||
if (scrollHeight === 0) return;
|
if (scrollHeight === 0) return;
|
||||||
|
|
||||||
onScrollDownToggle(isUnread ? !isAtBottom : !isNearBottom);
|
onScrollDownToggle?.(isUnread ? !isAtBottom : !isNearBottom);
|
||||||
onNotchToggle(!isAtBottom);
|
onNotchToggle?.(!isAtBottom);
|
||||||
});
|
});
|
||||||
|
|
||||||
const {
|
const {
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import type React from '../../lib/teact/teact';
|
import type { FC } from '../../lib/teact/teact';
|
||||||
import { memo } from '../../lib/teact/teact';
|
import { memo } from '../../lib/teact/teact';
|
||||||
import { withGlobal } from '../../global';
|
import { withGlobal } from '../../global';
|
||||||
|
|
||||||
@ -37,6 +37,7 @@ import PaidReactionModal from './paidReaction/PaidReactionModal.async';
|
|||||||
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
|
import PreparedMessageModal from './preparedMessage/PreparedMessageModal.async';
|
||||||
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
|
import PriceConfirmModal from './priceConfirm/PriceConfirmModal.async';
|
||||||
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
|
import ProfileRatingModal from './profileRating/ProfileRatingModal.async';
|
||||||
|
import QuickPreviewModal from './quickPreview/QuickPreviewModal.async';
|
||||||
import ReportAdModal from './reportAd/ReportAdModal.async';
|
import ReportAdModal from './reportAd/ReportAdModal.async';
|
||||||
import ReportModal from './reportModal/ReportModal.async';
|
import ReportModal from './reportModal/ReportModal.async';
|
||||||
import SharePreparedMessageModal from './sharePreparedMessage/SharePreparedMessageModal.async';
|
import SharePreparedMessageModal from './sharePreparedMessage/SharePreparedMessageModal.async';
|
||||||
@ -97,14 +98,15 @@ type ModalKey = keyof Pick<TabState,
|
|||||||
'isFrozenAccountModalOpen' |
|
'isFrozenAccountModalOpen' |
|
||||||
'deleteAccountModal' |
|
'deleteAccountModal' |
|
||||||
'isAgeVerificationModalOpen' |
|
'isAgeVerificationModalOpen' |
|
||||||
'profileRatingModal'
|
'profileRatingModal' |
|
||||||
|
'quickPreview'
|
||||||
>;
|
>;
|
||||||
|
|
||||||
type StateProps = {
|
type StateProps = {
|
||||||
[K in ModalKey]?: TabState[K];
|
[K in ModalKey]?: TabState[K];
|
||||||
};
|
};
|
||||||
type ModalRegistry = {
|
type ModalRegistry = {
|
||||||
[K in ModalKey]: React.FC<{
|
[K in ModalKey]: FC<{
|
||||||
modal: TabState[K];
|
modal: TabState[K];
|
||||||
}>;
|
}>;
|
||||||
};
|
};
|
||||||
@ -157,6 +159,7 @@ const MODALS: ModalRegistry = {
|
|||||||
deleteAccountModal: DeleteAccountModal,
|
deleteAccountModal: DeleteAccountModal,
|
||||||
isAgeVerificationModalOpen: AgeVerificationModal,
|
isAgeVerificationModalOpen: AgeVerificationModal,
|
||||||
profileRatingModal: ProfileRatingModal,
|
profileRatingModal: ProfileRatingModal,
|
||||||
|
quickPreview: QuickPreviewModal,
|
||||||
};
|
};
|
||||||
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
const MODAL_KEYS = Object.keys(MODALS) as ModalKey[];
|
||||||
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
const MODAL_ENTRIES = Object.entries(MODALS) as Entries<ModalRegistry>;
|
||||||
|
|||||||
@ -0,0 +1,18 @@
|
|||||||
|
import type { FC } from '../../../lib/teact/teact';
|
||||||
|
import { memo } from '../../../lib/teact/teact';
|
||||||
|
|
||||||
|
import type { OwnProps } from './QuickPreviewModal';
|
||||||
|
|
||||||
|
import { Bundles } from '../../../util/moduleLoader';
|
||||||
|
|
||||||
|
import useModuleLoader from '../../../hooks/useModuleLoader';
|
||||||
|
|
||||||
|
const QuickPreviewModalAsync: FC<OwnProps> = memo((props) => {
|
||||||
|
const { modal } = props;
|
||||||
|
|
||||||
|
const QuickPreviewModal = useModuleLoader(Bundles.Extra, 'QuickPreviewModal', !modal);
|
||||||
|
|
||||||
|
return QuickPreviewModal ? <QuickPreviewModal {...props} /> : undefined;
|
||||||
|
});
|
||||||
|
|
||||||
|
export default QuickPreviewModalAsync;
|
||||||
@ -0,0 +1,55 @@
|
|||||||
|
.root {
|
||||||
|
:global(.modal-dialog) {
|
||||||
|
overflow: hidden;
|
||||||
|
height: 40rem;
|
||||||
|
max-height: 90vh;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.content {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
|
||||||
|
height: 100%;
|
||||||
|
margin: 0 !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
|
||||||
|
:global(.messages-container) {
|
||||||
|
pointer-events: none;
|
||||||
|
|
||||||
|
// Prevent right column width calculation from applying
|
||||||
|
width: 100% !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.sticky-date span) {
|
||||||
|
pointer-events: none !important;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analogue of `#MiddleColumn` from MiddleColumn.module.scss
|
||||||
|
.column {
|
||||||
|
cursor: pointer;
|
||||||
|
|
||||||
|
position: relative;
|
||||||
|
z-index: 1;
|
||||||
|
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
|
||||||
|
min-width: 0;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Analogue of `.messages-layout` from MiddleColumn.module.scss
|
||||||
|
.messagesLayout {
|
||||||
|
position: relative;
|
||||||
|
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
133
src/components/modals/quickPreview/QuickPreviewModal.tsx
Normal file
133
src/components/modals/quickPreview/QuickPreviewModal.tsx
Normal file
@ -0,0 +1,133 @@
|
|||||||
|
import type { FC } from '@teact';
|
||||||
|
import { memo, useEffect } from '@teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { TabState } from '../../../global/types';
|
||||||
|
import type { ThemeKey } from '../../../types';
|
||||||
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
|
|
||||||
|
import { selectTheme, selectThemeValues } from '../../../global/selectors';
|
||||||
|
import buildClassName from '../../../util/buildClassName';
|
||||||
|
import buildStyle from '../../../util/buildStyle';
|
||||||
|
import captureEscKeyListener from '../../../util/captureEscKeyListener';
|
||||||
|
|
||||||
|
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||||
|
import useCustomBackground from '../../../hooks/useCustomBackground';
|
||||||
|
import useHistoryBack from '../../../hooks/useHistoryBack';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
|
||||||
|
import MessageList from '../../middle/MessageList';
|
||||||
|
import Modal from '../../ui/Modal';
|
||||||
|
import QuickPreviewModalHeader from './QuickPreviewModalHeader';
|
||||||
|
|
||||||
|
import backgroundStyles from '../../../styles/_patternBackground.module.scss';
|
||||||
|
import styles from './QuickPreviewModal.module.scss';
|
||||||
|
|
||||||
|
export type OwnProps = {
|
||||||
|
modal: TabState['quickPreview'];
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
theme: ThemeKey;
|
||||||
|
customBackground?: string;
|
||||||
|
backgroundColor?: string;
|
||||||
|
patternColor?: string;
|
||||||
|
isBackgroundBlurred?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const QuickPreviewModal: FC<OwnProps & StateProps> = ({
|
||||||
|
modal,
|
||||||
|
theme,
|
||||||
|
customBackground,
|
||||||
|
backgroundColor,
|
||||||
|
patternColor,
|
||||||
|
isBackgroundBlurred,
|
||||||
|
}) => {
|
||||||
|
const { closeQuickPreview, openChat, openThread } = getActions();
|
||||||
|
|
||||||
|
const chatId = modal?.chatId;
|
||||||
|
const threadId = modal?.threadId;
|
||||||
|
const isOpen = Boolean(chatId);
|
||||||
|
const customBackgroundValue = useCustomBackground(theme, customBackground);
|
||||||
|
|
||||||
|
const handleClose = useLastCallback(() => {
|
||||||
|
closeQuickPreview();
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleContentClick = useLastCallback(() => {
|
||||||
|
if (chatId) {
|
||||||
|
if (threadId) {
|
||||||
|
openThread({ chatId, threadId, shouldReplaceHistory: true });
|
||||||
|
} else {
|
||||||
|
openChat({ id: chatId, shouldReplaceHistory: true });
|
||||||
|
}
|
||||||
|
closeQuickPreview();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
useEffect(() => isOpen ? captureEscKeyListener(handleClose) : undefined, [isOpen, handleClose]);
|
||||||
|
|
||||||
|
useHistoryBack({
|
||||||
|
isActive: isOpen,
|
||||||
|
onBack: handleClose,
|
||||||
|
});
|
||||||
|
|
||||||
|
const { chatId: renderingChatId, threadId: renderingThreadId } = useCurrentOrPrev(modal, true)!;
|
||||||
|
|
||||||
|
const bgClassName = buildClassName(
|
||||||
|
backgroundStyles.background,
|
||||||
|
customBackground && backgroundStyles.customBgImage,
|
||||||
|
backgroundColor && backgroundStyles.customBgColor,
|
||||||
|
customBackground && isBackgroundBlurred && backgroundStyles.blurred,
|
||||||
|
);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Modal
|
||||||
|
isOpen={isOpen}
|
||||||
|
onClose={handleClose}
|
||||||
|
header={<QuickPreviewModalHeader chatId={renderingChatId} threadId={renderingThreadId} onClose={handleClose} />}
|
||||||
|
className={styles.root}
|
||||||
|
contentClassName={styles.content}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={styles.column}
|
||||||
|
style={buildStyle(
|
||||||
|
`--pattern-color: ${patternColor}`,
|
||||||
|
backgroundColor && `--theme-background-color: ${backgroundColor}`,
|
||||||
|
)}
|
||||||
|
onClick={handleContentClick}
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
className={bgClassName}
|
||||||
|
style={customBackgroundValue ? `--custom-background: ${customBackgroundValue}` : undefined}
|
||||||
|
/>
|
||||||
|
<div className={styles.messagesLayout}>
|
||||||
|
<MessageList
|
||||||
|
chatId={renderingChatId}
|
||||||
|
threadId={renderingThreadId || MAIN_THREAD_ID}
|
||||||
|
type="thread"
|
||||||
|
canPost={false}
|
||||||
|
isReady
|
||||||
|
withDefaultBg={Boolean(!customBackground && !backgroundColor)}
|
||||||
|
isQuickPreview
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>((global, { modal: chatId }): Complete<StateProps> => {
|
||||||
|
const theme = selectTheme(global);
|
||||||
|
const {
|
||||||
|
isBlurred: isBackgroundBlurred, background: customBackground, backgroundColor, patternColor,
|
||||||
|
} = selectThemeValues(global, theme) || {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
theme,
|
||||||
|
customBackground,
|
||||||
|
backgroundColor,
|
||||||
|
patternColor,
|
||||||
|
isBackgroundBlurred,
|
||||||
|
};
|
||||||
|
})(QuickPreviewModal));
|
||||||
@ -0,0 +1,98 @@
|
|||||||
|
.root {
|
||||||
|
position: relative;
|
||||||
|
min-height: 3rem;
|
||||||
|
padding: 0.5rem 0.625rem 0.625rem 0.75rem !important;
|
||||||
|
border-bottom: 1px solid var(--color-borders);
|
||||||
|
|
||||||
|
:global(.modal-title) {
|
||||||
|
margin: 0 !important;
|
||||||
|
font-size: inherit;
|
||||||
|
font-weight: inherit;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton, .markAsReadButton {
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.closeButton {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.markAsReadButton {
|
||||||
|
position: absolute !important;
|
||||||
|
top: 0.5rem;
|
||||||
|
right: 3rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.chatInfoOverride {
|
||||||
|
// Mimic left column chat list styling
|
||||||
|
:global(.ChatInfo) {
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
gap: 0.625rem;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.Avatar) {
|
||||||
|
flex-shrink: 0;
|
||||||
|
width: 2.625rem;
|
||||||
|
height: 2.625rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.info) {
|
||||||
|
overflow: hidden;
|
||||||
|
display: flex;
|
||||||
|
flex: 1;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.125rem;
|
||||||
|
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.title),
|
||||||
|
:global(.fullName) {
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
margin-bottom: 0;
|
||||||
|
|
||||||
|
font-size: 1rem;
|
||||||
|
font-weight: var(--font-weight-semibold);
|
||||||
|
line-height: 1.25rem;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.status) {
|
||||||
|
font-size: 0.875rem;
|
||||||
|
line-height: 1.125rem;
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.topic-header-icon) {
|
||||||
|
--custom-emoji-size: 2.25rem;
|
||||||
|
|
||||||
|
width: 2.5rem !important;
|
||||||
|
height: 2.5rem !important;
|
||||||
|
font-size: 2.25rem;
|
||||||
|
|
||||||
|
:global(.emoji-small) {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.topic-icon-letter) {
|
||||||
|
font-size: 1.25rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:global(.general-forum-icon) {
|
||||||
|
color: var(--color-text-secondary);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
:global(.Transition.message-count-transition) {
|
||||||
|
height: 1.125rem;
|
||||||
|
}
|
||||||
|
}
|
||||||
157
src/components/modals/quickPreview/QuickPreviewModalHeader.tsx
Normal file
157
src/components/modals/quickPreview/QuickPreviewModalHeader.tsx
Normal file
@ -0,0 +1,157 @@
|
|||||||
|
import type { FC } from '@teact';
|
||||||
|
import { memo } from '@teact';
|
||||||
|
import { getActions, withGlobal } from '../../../global';
|
||||||
|
|
||||||
|
import type { ApiChat, ApiTypingStatus, ApiUpdateConnectionStateType } from '../../../api/types';
|
||||||
|
import type { ThreadId } from '../../../types';
|
||||||
|
import { MAIN_THREAD_ID } from '../../../api/types';
|
||||||
|
|
||||||
|
import { getIsSavedDialog } from '../../../global/helpers';
|
||||||
|
import { selectChat, selectThreadParam, selectTopic } from '../../../global/selectors';
|
||||||
|
import { isUserId } from '../../../util/entities/ids';
|
||||||
|
|
||||||
|
import useConnectionStatus from '../../../hooks/useConnectionStatus';
|
||||||
|
import useLang from '../../../hooks/useLang';
|
||||||
|
import useLastCallback from '../../../hooks/useLastCallback';
|
||||||
|
import useOldLang from '../../../hooks/useOldLang';
|
||||||
|
|
||||||
|
import GroupChatInfo from '../../common/GroupChatInfo';
|
||||||
|
import Icon from '../../common/icons/Icon';
|
||||||
|
import PrivateChatInfo from '../../common/PrivateChatInfo';
|
||||||
|
import Button from '../../ui/Button';
|
||||||
|
|
||||||
|
import styles from './QuickPreviewModalHeader.module.scss';
|
||||||
|
|
||||||
|
type OwnProps = {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: ThreadId;
|
||||||
|
onClose: VoidFunction;
|
||||||
|
};
|
||||||
|
|
||||||
|
type StateProps = {
|
||||||
|
chat?: ApiChat;
|
||||||
|
connectionState?: ApiUpdateConnectionStateType;
|
||||||
|
isSyncing?: boolean;
|
||||||
|
isFetchingDifference?: boolean;
|
||||||
|
typingStatus?: ApiTypingStatus;
|
||||||
|
isSavedDialog?: boolean;
|
||||||
|
unreadCount?: number;
|
||||||
|
hasUnreadMark?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const EMOJI_STATUS_SIZE = 20;
|
||||||
|
|
||||||
|
const QuickPreviewModalHeader: FC<OwnProps & StateProps> = ({
|
||||||
|
chatId,
|
||||||
|
threadId,
|
||||||
|
chat,
|
||||||
|
connectionState,
|
||||||
|
isSyncing,
|
||||||
|
isFetchingDifference,
|
||||||
|
typingStatus,
|
||||||
|
isSavedDialog,
|
||||||
|
unreadCount,
|
||||||
|
hasUnreadMark,
|
||||||
|
onClose,
|
||||||
|
}) => {
|
||||||
|
const lang = useLang();
|
||||||
|
const oldLang = useOldLang();
|
||||||
|
const { markChatMessagesRead } = getActions();
|
||||||
|
const {
|
||||||
|
connectionStatusText,
|
||||||
|
} = useConnectionStatus(oldLang, connectionState, isSyncing || isFetchingDifference, true);
|
||||||
|
|
||||||
|
const handleMarkAsRead = useLastCallback(() => {
|
||||||
|
markChatMessagesRead({ id: chatId });
|
||||||
|
});
|
||||||
|
|
||||||
|
const savedMessagesStatus = isSavedDialog ? lang('SavedMessages') : undefined;
|
||||||
|
const realChatId = isSavedDialog ? String(MAIN_THREAD_ID) : chatId;
|
||||||
|
const displayChatId = chat?.isMonoforum ? chat.linkedMonoforumId! : realChatId;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className={styles.root}>
|
||||||
|
{Boolean(unreadCount || hasUnreadMark) && (
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
ariaLabel={lang('ChatListContextMaskAsRead')}
|
||||||
|
onClick={handleMarkAsRead}
|
||||||
|
className={styles.markAsReadButton}
|
||||||
|
>
|
||||||
|
<Icon name="readchats" />
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
<Button
|
||||||
|
round
|
||||||
|
color="translucent"
|
||||||
|
size="smaller"
|
||||||
|
ariaLabel={lang('Close')}
|
||||||
|
onClick={onClose}
|
||||||
|
className={styles.closeButton}
|
||||||
|
>
|
||||||
|
<Icon name="close" />
|
||||||
|
</Button>
|
||||||
|
<div className="modal-title">
|
||||||
|
<div className={styles.chatInfoOverride}>
|
||||||
|
{isUserId(displayChatId) ? (
|
||||||
|
<PrivateChatInfo
|
||||||
|
key={displayChatId}
|
||||||
|
userId={displayChatId}
|
||||||
|
typingStatus={typingStatus}
|
||||||
|
status={connectionStatusText || savedMessagesStatus}
|
||||||
|
withDots={Boolean(connectionStatusText)}
|
||||||
|
withFullInfo={false}
|
||||||
|
withMediaViewer={false}
|
||||||
|
withStory={false}
|
||||||
|
withUpdatingStatus
|
||||||
|
isSavedDialog={isSavedDialog}
|
||||||
|
emojiStatusSize={EMOJI_STATUS_SIZE}
|
||||||
|
noRtl
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<GroupChatInfo
|
||||||
|
key={displayChatId}
|
||||||
|
chatId={displayChatId}
|
||||||
|
threadId={!isSavedDialog ? threadId : undefined}
|
||||||
|
typingStatus={typingStatus}
|
||||||
|
withMonoforumStatus={chat?.isMonoforum}
|
||||||
|
status={connectionStatusText || savedMessagesStatus}
|
||||||
|
withDots={Boolean(connectionStatusText)}
|
||||||
|
withMediaViewer={false}
|
||||||
|
withFullInfo={false}
|
||||||
|
withUpdatingStatus
|
||||||
|
withStory={false}
|
||||||
|
isSavedDialog={isSavedDialog}
|
||||||
|
emojiStatusSize={EMOJI_STATUS_SIZE}
|
||||||
|
noRtl
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default memo(withGlobal<OwnProps>(
|
||||||
|
(global, { chatId, threadId }): Complete<StateProps> => {
|
||||||
|
const chat = selectChat(global, chatId);
|
||||||
|
const typingStatus = selectThreadParam(global, chatId, threadId || MAIN_THREAD_ID, 'typingStatus');
|
||||||
|
const isSavedDialog = getIsSavedDialog(chatId, threadId || MAIN_THREAD_ID, global.currentUserId);
|
||||||
|
const unreadCount = chat?.isForum && threadId
|
||||||
|
? selectTopic(global, chatId, threadId)?.unreadCount
|
||||||
|
: chat?.unreadCount;
|
||||||
|
|
||||||
|
return {
|
||||||
|
chat,
|
||||||
|
connectionState: global.connectionState,
|
||||||
|
isSyncing: global.isSyncing,
|
||||||
|
isFetchingDifference: global.isFetchingDifference,
|
||||||
|
typingStatus,
|
||||||
|
isSavedDialog,
|
||||||
|
unreadCount,
|
||||||
|
hasUnreadMark: chat?.hasUnreadMark,
|
||||||
|
};
|
||||||
|
},
|
||||||
|
)(QuickPreviewModalHeader));
|
||||||
@ -1180,3 +1180,19 @@ addActionHandler('updateSharePreparedMessageModalSendArgs', async (global, actio
|
|||||||
}, tabId);
|
}, tabId);
|
||||||
setGlobal(global);
|
setGlobal(global);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addActionHandler('openQuickPreview', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { id: chatId, threadId, tabId = getCurrentTabId() } = payload;
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
quickPreview: { chatId, threadId },
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|
||||||
|
addActionHandler('closeQuickPreview', (global, actions, payload): ActionReturnType => {
|
||||||
|
const { tabId = getCurrentTabId() } = payload || {};
|
||||||
|
|
||||||
|
return updateTabState(global, {
|
||||||
|
quickPreview: undefined,
|
||||||
|
}, tabId);
|
||||||
|
});
|
||||||
|
|||||||
@ -2509,6 +2509,12 @@ export interface ActionPayloads {
|
|||||||
} & WithTabId;
|
} & WithTabId;
|
||||||
closeSuggestedPostApprovalModal: WithTabId | undefined;
|
closeSuggestedPostApprovalModal: WithTabId | undefined;
|
||||||
|
|
||||||
|
openQuickPreview: {
|
||||||
|
id: string;
|
||||||
|
threadId?: ThreadId;
|
||||||
|
} & WithTabId;
|
||||||
|
closeQuickPreview: WithTabId | undefined;
|
||||||
|
|
||||||
openDeleteMessageModal: ({
|
openDeleteMessageModal: ({
|
||||||
chatId: string;
|
chatId: string;
|
||||||
messageIds: number[];
|
messageIds: number[];
|
||||||
|
|||||||
@ -875,6 +875,11 @@ export type TabState = {
|
|||||||
errorKey?: RegularLangFnParameters;
|
errorKey?: RegularLangFnParameters;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
quickPreview?: {
|
||||||
|
chatId: string;
|
||||||
|
threadId?: ThreadId;
|
||||||
|
};
|
||||||
|
|
||||||
isWaitingForStarGiftUpgrade?: true;
|
isWaitingForStarGiftUpgrade?: true;
|
||||||
isWaitingForStarGiftTransfer?: true;
|
isWaitingForStarGiftTransfer?: true;
|
||||||
insertingPeerIdMention?: string;
|
insertingPeerIdMention?: string;
|
||||||
|
|||||||
@ -5,9 +5,7 @@ import type { ApiChat, ApiTopic, ApiUser } from '../api/types';
|
|||||||
import type { MenuItemContextAction } from '../components/ui/ListItem';
|
import type { MenuItemContextAction } from '../components/ui/ListItem';
|
||||||
|
|
||||||
import { SERVICE_NOTIFICATIONS_USER_ID } from '../config';
|
import { SERVICE_NOTIFICATIONS_USER_ID } from '../config';
|
||||||
import {
|
import { getCanDeleteChat, isChatArchived, isChatChannel, isChatGroup } from '../global/helpers';
|
||||||
getCanDeleteChat, isChatArchived, isChatChannel, isChatGroup,
|
|
||||||
} from '../global/helpers';
|
|
||||||
import { IS_TAURI } from '../util/browser/globalEnvironment';
|
import { IS_TAURI } from '../util/browser/globalEnvironment';
|
||||||
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/browser/windowEnvironment';
|
import { IS_OPEN_IN_NEW_TAB_SUPPORTED } from '../util/browser/windowEnvironment';
|
||||||
import { isUserId } from '../util/entities/ids';
|
import { isUserId } from '../util/entities/ids';
|
||||||
@ -86,6 +84,7 @@ const useChatContextActions = ({
|
|||||||
markChatMessagesRead,
|
markChatMessagesRead,
|
||||||
markChatUnread,
|
markChatUnread,
|
||||||
openChatInNewTab,
|
openChatInNewTab,
|
||||||
|
openQuickPreview,
|
||||||
} = getActions();
|
} = getActions();
|
||||||
|
|
||||||
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
|
const actionOpenInNewTab = IS_OPEN_IN_NEW_TAB_SUPPORTED && {
|
||||||
@ -100,6 +99,16 @@ const useChatContextActions = ({
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const actionQuickPreview = !isSavedDialog && !chat.isForum && {
|
||||||
|
title: lang('QuickPreview'),
|
||||||
|
icon: 'eye-outline',
|
||||||
|
handler: () => {
|
||||||
|
openQuickPreview({
|
||||||
|
id: chat.id,
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
const togglePinned = () => {
|
const togglePinned = () => {
|
||||||
if (isSavedDialog) {
|
if (isSavedDialog) {
|
||||||
toggleSavedDialogPinned({ id: chat.id });
|
toggleSavedDialogPinned({ id: chat.id });
|
||||||
@ -128,7 +137,7 @@ const useChatContextActions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isSavedDialog) {
|
if (isSavedDialog) {
|
||||||
return compact([actionOpenInNewTab, actionPin, actionDelete]) as MenuItemContextAction[];
|
return compact([actionOpenInNewTab, actionQuickPreview, actionPin, actionDelete]) as MenuItemContextAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionAddToFolder = canChangeFolder ? {
|
const actionAddToFolder = canChangeFolder ? {
|
||||||
@ -150,12 +159,15 @@ const useChatContextActions = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (isInSearch) {
|
if (isInSearch) {
|
||||||
return compact([actionOpenInNewTab, actionPin, actionAddToFolder, actionMute]) as MenuItemContextAction[];
|
return compact([
|
||||||
|
actionOpenInNewTab, actionQuickPreview, actionPin, actionAddToFolder, actionMute,
|
||||||
|
]) as MenuItemContextAction[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const actionMaskAsRead = (
|
const actionMaskAsRead = (
|
||||||
chat.unreadCount || chat.hasUnreadMark || Object.values(topics || {}).some(({ unreadCount }) => unreadCount)
|
chat.unreadCount || chat.hasUnreadMark || Object.values(topics || {}).some(({ unreadCount }) => unreadCount)
|
||||||
) ? {
|
)
|
||||||
|
? {
|
||||||
title: lang('ChatListContextMaskAsRead'),
|
title: lang('ChatListContextMaskAsRead'),
|
||||||
icon: 'readchats',
|
icon: 'readchats',
|
||||||
handler: () => markChatMessagesRead({ id: chat.id }),
|
handler: () => markChatMessagesRead({ id: chat.id }),
|
||||||
@ -177,6 +189,7 @@ const useChatContextActions = ({
|
|||||||
|
|
||||||
return compact([
|
return compact([
|
||||||
actionOpenInNewTab,
|
actionOpenInNewTab,
|
||||||
|
actionQuickPreview,
|
||||||
actionAddToFolder,
|
actionAddToFolder,
|
||||||
actionMaskAsRead,
|
actionMaskAsRead,
|
||||||
actionMarkAsUnread,
|
actionMarkAsUnread,
|
||||||
|
|||||||
@ -1,24 +1,26 @@
|
|||||||
import { useCallback, useEffect, useRef } from '../lib/teact/teact';
|
import { useEffect, useRef } from '../lib/teact/teact';
|
||||||
|
|
||||||
|
import useLastCallback from './useLastCallback.ts';
|
||||||
|
|
||||||
const useSendWithEnter = (
|
const useSendWithEnter = (
|
||||||
onSelect: NoneToVoidFunction,
|
onSelect: NoneToVoidFunction,
|
||||||
) => {
|
) => {
|
||||||
const buttonRef = useRef<HTMLDivElement>();
|
const buttonRef = useRef<HTMLDivElement>();
|
||||||
|
|
||||||
const handleKeyDown = useCallback((e: KeyboardEvent) => {
|
const handleKeyDown = useLastCallback((e: KeyboardEvent) => {
|
||||||
if (e.key !== 'Enter') return;
|
if (e.key !== 'Enter') return;
|
||||||
const isFocused = buttonRef.current === document.activeElement;
|
const isFocused = buttonRef.current === document.activeElement;
|
||||||
|
|
||||||
if (isFocused) {
|
if (isFocused) {
|
||||||
onSelect();
|
onSelect();
|
||||||
}
|
}
|
||||||
}, [onSelect]);
|
});
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
window.addEventListener('keydown', handleKeyDown, false);
|
window.addEventListener('keydown', handleKeyDown, false);
|
||||||
|
|
||||||
return () => window.removeEventListener('keydown', handleKeyDown, false);
|
return () => window.removeEventListener('keydown', handleKeyDown, false);
|
||||||
}, [handleKeyDown]);
|
}, []);
|
||||||
|
|
||||||
return buttonRef;
|
return buttonRef;
|
||||||
};
|
};
|
||||||
|
|||||||
@ -25,7 +25,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
:global(html.theme-light) &:not(.customBgImage)::before {
|
:global(html.theme-light) &:not(.customBgImage)::before {
|
||||||
background-image: url('../../assets/chat-bg-br.png');
|
background-image: url('../assets/chat-bg-br.png');
|
||||||
}
|
}
|
||||||
|
|
||||||
&:not(.customBgImage).customBgColor::before {
|
&:not(.customBgImage).customBgColor::before {
|
||||||
@ -77,14 +77,14 @@
|
|||||||
bottom: 0;
|
bottom: 0;
|
||||||
left: 0;
|
left: 0;
|
||||||
|
|
||||||
background-image: url('../../assets/chat-bg-pattern-light.png');
|
background-image: url('../assets/chat-bg-pattern-light.png');
|
||||||
background-repeat: repeat;
|
background-repeat: repeat;
|
||||||
background-position: top right;
|
background-position: top right;
|
||||||
background-size: 510px auto;
|
background-size: 510px auto;
|
||||||
mix-blend-mode: overlay;
|
mix-blend-mode: overlay;
|
||||||
|
|
||||||
:global(html.theme-dark) & {
|
:global(html.theme-dark) & {
|
||||||
background-image: url('../../assets/chat-bg-pattern-dark.png');
|
background-image: url('../assets/chat-bg-pattern-dark.png');
|
||||||
mix-blend-mode: unset;
|
mix-blend-mode: unset;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
1
src/types/language.d.ts
vendored
1
src/types/language.d.ts
vendored
@ -1705,6 +1705,7 @@ export interface LangPair {
|
|||||||
'GiftValueForSaleOnFragment': undefined;
|
'GiftValueForSaleOnFragment': undefined;
|
||||||
'GiftValueForSaleOnTelegram': undefined;
|
'GiftValueForSaleOnTelegram': undefined;
|
||||||
'EmbeddedMessageNoCaption': undefined;
|
'EmbeddedMessageNoCaption': undefined;
|
||||||
|
'QuickPreview': undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface LangPairWithVariables<V = LangVariable> {
|
export interface LangPairWithVariables<V = LangVariable> {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user