Web Page: Send inverted web page; Web Page menu (#4701)
This commit is contained in:
parent
cf4c562408
commit
5fb1283884
@ -25,6 +25,7 @@ import type {
|
||||
ApiTopic,
|
||||
ApiUser,
|
||||
ApiVideo,
|
||||
ApiWebPage,
|
||||
} from '../../api/types';
|
||||
import type {
|
||||
ApiDraft, GlobalState, MessageList,
|
||||
@ -75,6 +76,7 @@ import {
|
||||
selectIsReactionPickerOpen,
|
||||
selectIsRightColumnShown,
|
||||
selectNewestMessageWithBotKeyboardButtons,
|
||||
selectNoWebPage,
|
||||
selectPeerStory,
|
||||
selectRequestedDraft,
|
||||
selectRequestedDraftFiles,
|
||||
@ -249,6 +251,8 @@ type StateProps =
|
||||
quickReplyMessages?: Record<number, ApiMessage>;
|
||||
quickReplies?: Record<number, ApiQuickReply>;
|
||||
canSendQuickReplies?: boolean;
|
||||
webPagePreview?: ApiWebPage;
|
||||
noWebPage?: boolean;
|
||||
};
|
||||
|
||||
enum MainButtonState {
|
||||
@ -355,6 +359,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
quickReplies,
|
||||
canSendQuickReplies,
|
||||
onForward,
|
||||
webPagePreview,
|
||||
noWebPage,
|
||||
}) => {
|
||||
const {
|
||||
sendMessage,
|
||||
@ -377,6 +383,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
closeReactionPicker,
|
||||
sendStoryReaction,
|
||||
editMessage,
|
||||
updateAttachmentSettings,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
@ -466,8 +473,15 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
[chat, chatFullInfo, isChatWithBot, isInStoryViewer],
|
||||
);
|
||||
|
||||
const hasWebPagePreview = !hasAttachments && canAttachEmbedLinks && !noWebPage && Boolean(webPagePreview);
|
||||
const isComposerBlocked = !canSendPlainText && !editingMessage;
|
||||
|
||||
useEffect(() => {
|
||||
if (!hasWebPagePreview) {
|
||||
updateAttachmentSettings({ isInvertedMedia: undefined });
|
||||
}
|
||||
}, [hasWebPagePreview]);
|
||||
|
||||
const insertHtmlAndUpdateCursor = useLastCallback((newHtml: string, inInputId: string = editableInputId) => {
|
||||
if (inInputId === editableInputId && isComposerBlocked) return;
|
||||
const selection = window.getSelection()!;
|
||||
@ -1012,6 +1026,8 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
if (text) {
|
||||
if (!checkSlowMode()) return;
|
||||
|
||||
const isInvertedMedia = hasWebPagePreview ? attachmentSettings.isInvertedMedia : undefined;
|
||||
|
||||
sendMessage({
|
||||
messageList: currentMessageList,
|
||||
text,
|
||||
@ -1019,6 +1035,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
scheduledAt,
|
||||
isSilent,
|
||||
shouldUpdateStickerSetOrder,
|
||||
isInvertedMedia,
|
||||
});
|
||||
}
|
||||
|
||||
@ -1684,6 +1701,7 @@ const Composer: FC<OwnProps & StateProps> = ({
|
||||
threadId={threadId}
|
||||
getHtml={getHtml}
|
||||
isDisabled={!canAttachEmbedLinks || hasAttachments}
|
||||
isEditing={Boolean(editingMessage)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
@ -2048,6 +2066,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
|
||||
const canSendQuickReplies = isChatWithUser && !isChatWithBot && !isInScheduledList && !isChatWithSelf;
|
||||
|
||||
const noWebPage = selectNoWebPage(global, chatId, threadId);
|
||||
|
||||
return {
|
||||
availableReactions: type === 'story' ? global.reactions.availableReactions : undefined,
|
||||
topReactions: type === 'story' ? global.reactions.topReactions : undefined,
|
||||
@ -2115,6 +2135,8 @@ export default memo(withGlobal<OwnProps>(
|
||||
quickReplyMessages: global.quickReplies.messagesById,
|
||||
quickReplies: global.quickReplies.byId,
|
||||
canSendQuickReplies,
|
||||
noWebPage,
|
||||
webPagePreview: selectTabState(global).webPagePreview,
|
||||
};
|
||||
},
|
||||
)(Composer));
|
||||
|
||||
@ -167,7 +167,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
(shouldSendCompressed || shouldForceCompression || isInAlbum) && !shouldForceAsFile,
|
||||
);
|
||||
const [shouldSendGrouped, setShouldSendGrouped] = useState(attachmentSettings.shouldSendGrouped);
|
||||
const [isInvertedMedia, setIsInvertedMedia] = useState(attachmentSettings.isInvertedMedia);
|
||||
const isInvertedMedia = attachmentSettings.isInvertedMedia;
|
||||
|
||||
const {
|
||||
handleScroll: handleAttachmentsScroll,
|
||||
@ -184,6 +184,7 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
closeSymbolMenu();
|
||||
updateAttachmentSettings({ isInvertedMedia: undefined });
|
||||
}
|
||||
}, [closeSymbolMenu, isOpen]);
|
||||
|
||||
@ -254,10 +255,19 @@ const AttachmentModal: FC<OwnProps & StateProps> = ({
|
||||
if (isOpen) {
|
||||
setShouldSendCompressed(shouldSuggestCompression ?? attachmentSettings.shouldCompress);
|
||||
setShouldSendGrouped(attachmentSettings.shouldSendGrouped);
|
||||
setIsInvertedMedia(attachmentSettings.isInvertedMedia);
|
||||
}
|
||||
}, [attachmentSettings, isOpen, shouldSuggestCompression]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isOpen) {
|
||||
updateAttachmentSettings({ isInvertedMedia: undefined });
|
||||
}
|
||||
}, [updateAttachmentSettings, isOpen, shouldSuggestCompression]);
|
||||
|
||||
function setIsInvertedMedia(value?: true) {
|
||||
updateAttachmentSettings({ isInvertedMedia: value });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (isOpen && isMobile) {
|
||||
removeAllSelections();
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
.WebPagePreview {
|
||||
position: relative;
|
||||
height: 3.125rem;
|
||||
/* stylelint-disable-next-line plugin/no-low-performance-animation-properties */
|
||||
transition: height 150ms ease-out, opacity 150ms ease-out;
|
||||
@ -30,6 +31,18 @@
|
||||
}
|
||||
}
|
||||
|
||||
.web-page-preview-context-menu {
|
||||
position: absolute;
|
||||
|
||||
.bubble {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.icon-placeholder {
|
||||
width: 1.5rem;
|
||||
}
|
||||
}
|
||||
|
||||
& &-left-icon {
|
||||
flex-shrink: 0;
|
||||
background: none !important;
|
||||
|
||||
@ -1,10 +1,13 @@
|
||||
import type { FC } from '../../../lib/teact/teact';
|
||||
import React, { memo, useEffect, useRef } from '../../../lib/teact/teact';
|
||||
import React, {
|
||||
memo, useEffect, useRef,
|
||||
} from '../../../lib/teact/teact';
|
||||
import { getActions, withGlobal } from '../../../global';
|
||||
|
||||
import type {
|
||||
ApiFormattedText, ApiMessage, ApiMessageEntityTextUrl, ApiWebPage,
|
||||
} from '../../../api/types';
|
||||
import type { GlobalState } from '../../../global/types';
|
||||
import type { ISettings, ThreadId } from '../../../types';
|
||||
import type { Signal } from '../../../util/signals';
|
||||
import { ApiMessageEntityTypes } from '../../../api/types';
|
||||
@ -15,14 +18,19 @@ import buildClassName from '../../../util/buildClassName';
|
||||
import parseHtmlAsFormattedText from '../../../util/parseHtmlAsFormattedText';
|
||||
|
||||
import { useDebouncedResolver } from '../../../hooks/useAsyncResolvers';
|
||||
import useContextMenuHandlers from '../../../hooks/useContextMenuHandlers';
|
||||
import useCurrentOrPrev from '../../../hooks/useCurrentOrPrev';
|
||||
import useDerivedSignal from '../../../hooks/useDerivedSignal';
|
||||
import useDerivedState from '../../../hooks/useDerivedState';
|
||||
import useLastCallback from '../../../hooks/useLastCallback';
|
||||
import useMenuPosition from '../../../hooks/useMenuPosition';
|
||||
import useOldLang from '../../../hooks/useOldLang';
|
||||
import useShowTransition from '../../../hooks/useShowTransition';
|
||||
import useSyncEffect from '../../../hooks/useSyncEffect';
|
||||
|
||||
import Button from '../../ui/Button';
|
||||
import Menu from '../../ui/Menu';
|
||||
import MenuItem from '../../ui/MenuItem';
|
||||
import WebPage from '../message/WebPage';
|
||||
|
||||
import './WebPagePreview.scss';
|
||||
@ -31,6 +39,7 @@ type OwnProps = {
|
||||
chatId: string;
|
||||
threadId: ThreadId;
|
||||
getHtml: Signal<string>;
|
||||
isEditing: boolean;
|
||||
isDisabled?: boolean;
|
||||
};
|
||||
|
||||
@ -38,6 +47,7 @@ type StateProps = {
|
||||
webPagePreview?: ApiWebPage;
|
||||
noWebPage?: boolean;
|
||||
theme: ISettings['theme'];
|
||||
attachmentSettings: GlobalState['attachmentSettings'];
|
||||
};
|
||||
|
||||
const DEBOUNCE_MS = 300;
|
||||
@ -51,15 +61,25 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
webPagePreview,
|
||||
noWebPage,
|
||||
theme,
|
||||
attachmentSettings,
|
||||
isEditing,
|
||||
}) => {
|
||||
const {
|
||||
loadWebPagePreview,
|
||||
clearWebPagePreview,
|
||||
toggleMessageWebPage,
|
||||
updateAttachmentSettings,
|
||||
} = getActions();
|
||||
|
||||
const lang = useOldLang();
|
||||
|
||||
const formattedTextWithLinkRef = useRef<ApiFormattedText>();
|
||||
|
||||
// eslint-disable-next-line no-null/no-null
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
|
||||
const isInvertedMedia = attachmentSettings.isInvertedMedia;
|
||||
|
||||
const detectLinkDebounced = useDebouncedResolver(() => {
|
||||
const formattedText = parseHtmlAsFormattedText(getHtml());
|
||||
const linkEntity = formattedText.entities?.find((entity): entity is ApiMessageEntityTextUrl => (
|
||||
@ -101,6 +121,41 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
toggleMessageWebPage({ chatId, threadId, noWebPage: true });
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuPosition, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, isEditing, true);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => ref.current!);
|
||||
const getMenuElement = useLastCallback(
|
||||
() => ref.current!.querySelector('.web-page-preview-context-menu .bubble'),
|
||||
);
|
||||
|
||||
const {
|
||||
positionX, positionY, transformOriginX, transformOriginY, style: menuStyle,
|
||||
} = useMenuPosition(
|
||||
contextMenuPosition,
|
||||
getTriggerElement,
|
||||
getRootElement,
|
||||
getMenuElement,
|
||||
);
|
||||
|
||||
const handlePreviewClick = useLastCallback((e: React.MouseEvent): void => {
|
||||
handleContextMenu(e);
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!shouldRender || !renderingWebPage) {
|
||||
handleContextMenuClose();
|
||||
handleContextMenuHide();
|
||||
}
|
||||
}, [handleContextMenuClose, handleContextMenuHide, shouldRender, renderingWebPage]);
|
||||
|
||||
function updateIsInvertedMedia(value?: true) {
|
||||
updateAttachmentSettings({ isInvertedMedia: value });
|
||||
}
|
||||
|
||||
if (!shouldRender || !renderingWebPage) {
|
||||
return undefined;
|
||||
}
|
||||
@ -113,13 +168,59 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
},
|
||||
} as ApiMessage;
|
||||
|
||||
function renderContextMenu() {
|
||||
return (
|
||||
<Menu
|
||||
isOpen={isContextMenuOpen}
|
||||
transformOriginX={transformOriginX}
|
||||
transformOriginY={transformOriginY}
|
||||
positionX={positionX}
|
||||
positionY={positionY}
|
||||
style={menuStyle}
|
||||
className="web-page-preview-context-menu"
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
autoClose
|
||||
>
|
||||
<>
|
||||
{
|
||||
isInvertedMedia ? (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="move-caption-up" onClick={() => updateIsInvertedMedia(undefined)}>
|
||||
{lang('PreviewSender.MoveTextUp')}
|
||||
</MenuItem>
|
||||
) : (
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
<MenuItem icon="move-caption-down" onClick={() => updateIsInvertedMedia(true)}>
|
||||
{lang(('PreviewSender.MoveTextDown'))}
|
||||
</MenuItem>
|
||||
)
|
||||
}
|
||||
<MenuItem
|
||||
icon="delete"
|
||||
// eslint-disable-next-line react/jsx-no-bind
|
||||
onClick={handleClearWebpagePreview}
|
||||
>
|
||||
{lang('ChatInput.EditLink.RemovePreview')}
|
||||
</MenuItem>
|
||||
</>
|
||||
</Menu>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={buildClassName('WebPagePreview', transitionClassNames)}>
|
||||
<div className={buildClassName('WebPagePreview', transitionClassNames)} ref={ref}>
|
||||
<div className="WebPagePreview_inner">
|
||||
<div className="WebPagePreview-left-icon">
|
||||
<div className="WebPagePreview-left-icon" onClick={handlePreviewClick}>
|
||||
<i className="icon icon-link" />
|
||||
</div>
|
||||
<WebPage message={messageStub} inPreview theme={theme} />
|
||||
<WebPage
|
||||
message={messageStub}
|
||||
inPreview
|
||||
theme={theme}
|
||||
onContainerClick={handlePreviewClick}
|
||||
isEditing={isEditing}
|
||||
/>
|
||||
<Button
|
||||
className="WebPagePreview-clear"
|
||||
round
|
||||
@ -130,6 +231,7 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
>
|
||||
<i className="icon icon-close" />
|
||||
</Button>
|
||||
{!isEditing && renderContextMenu()}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@ -138,10 +240,14 @@ const WebPagePreview: FC<OwnProps & StateProps> = ({
|
||||
export default memo(withGlobal<OwnProps>(
|
||||
(global, { chatId, threadId }): StateProps => {
|
||||
const noWebPage = selectNoWebPage(global, chatId, threadId);
|
||||
const {
|
||||
attachmentSettings,
|
||||
} = global;
|
||||
return {
|
||||
theme: selectTheme(global),
|
||||
webPagePreview: selectTabState(global).webPagePreview,
|
||||
noWebPage,
|
||||
attachmentSettings,
|
||||
};
|
||||
},
|
||||
)(WebPagePreview));
|
||||
|
||||
@ -49,6 +49,13 @@
|
||||
margin: 0;
|
||||
padding: 0.125rem 0.25rem 0.125rem 0.625rem;
|
||||
transition: background-color 0.2s ease-in;
|
||||
|
||||
&.interactive {
|
||||
cursor: var(--custom-cursor, pointer);
|
||||
&:active {
|
||||
background-color: var(--background-active-color);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&::before {
|
||||
|
||||
@ -56,6 +56,8 @@ type OwnProps = {
|
||||
onAudioPlay?: NoneToVoidFunction;
|
||||
onMediaClick?: NoneToVoidFunction;
|
||||
onCancelMediaTransfer?: NoneToVoidFunction;
|
||||
onContainerClick?: ((e: React.MouseEvent) => void);
|
||||
isEditing?: boolean;
|
||||
};
|
||||
|
||||
const WebPage: FC<OwnProps> = ({
|
||||
@ -76,8 +78,10 @@ const WebPage: FC<OwnProps> = ({
|
||||
shouldWarnAboutSvg,
|
||||
autoLoadFileMaxSizeMb,
|
||||
onMediaClick,
|
||||
onContainerClick,
|
||||
onAudioPlay,
|
||||
onCancelMediaTransfer,
|
||||
isEditing,
|
||||
}) => {
|
||||
const { openTelegramLink } = getActions();
|
||||
const webPage = getMessageWebPage(message);
|
||||
@ -92,6 +96,9 @@ const WebPage: FC<OwnProps> = ({
|
||||
const handleMediaClick = useLastCallback(() => {
|
||||
onMediaClick!();
|
||||
});
|
||||
const handleContainerClick = useLastCallback((e: React.MouseEvent) => {
|
||||
onContainerClick?.(e);
|
||||
});
|
||||
|
||||
const handleQuickButtonClick = useLastCallback(() => {
|
||||
if (!webPage) return;
|
||||
@ -138,6 +145,7 @@ const WebPage: FC<OwnProps> = ({
|
||||
const className = buildClassName(
|
||||
'WebPage',
|
||||
inPreview && 'in-preview',
|
||||
!isEditing && inPreview && 'interactive',
|
||||
isSquarePhoto && 'with-square-photo',
|
||||
!photo && !video && !inPreview && 'without-media',
|
||||
video && 'with-video',
|
||||
@ -166,6 +174,7 @@ const WebPage: FC<OwnProps> = ({
|
||||
className={className}
|
||||
data-initial={(siteName || displayUrl)[0]}
|
||||
dir={lang.isRtl ? 'rtl' : 'auto'}
|
||||
onClick={handleContainerClick}
|
||||
>
|
||||
<div className={buildClassName('WebPage--content', isStory && 'is-story')}>
|
||||
{backgroundEmojiId && (
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user