Profile: Add Focus Message button for entities in tabs (#6949)
This commit is contained in:
parent
480b09e9aa
commit
33c8b7d958
@ -3,6 +3,12 @@
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
|
||||
&.has-menu-open {
|
||||
border-radius: var(--border-radius-default);
|
||||
background-color: var(--color-chat-hover);
|
||||
box-shadow: 0 0 0 0.5rem var(--color-chat-hover);
|
||||
}
|
||||
|
||||
&.inline {
|
||||
margin-top: calc(0.5rem - 0.3125rem);
|
||||
|
||||
|
||||
@ -12,6 +12,7 @@ import type { BufferedRange } from '../../hooks/useBuffering';
|
||||
import type { OldLangFn } from '../../hooks/useOldLang';
|
||||
import type { ThemeKey } from '../../types';
|
||||
import type { LangFn } from '../../util/localization';
|
||||
import type { MenuItemContextAction } from '../ui/ListItem';
|
||||
import { ApiMediaFormat } from '../../api/types';
|
||||
import { AudioOrigin } from '../../types';
|
||||
|
||||
@ -38,6 +39,7 @@ import { MAX_EMPTY_WAVEFORM_POINTS, renderWaveform } from './helpers/waveform';
|
||||
import useAppLayout from '../../hooks/useAppLayout';
|
||||
import useAudioPlayer from '../../hooks/useAudioPlayer';
|
||||
import useBuffering from '../../hooks/useBuffering';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
@ -47,6 +49,9 @@ import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated
|
||||
|
||||
import Button from '../ui/Button';
|
||||
import Link from '../ui/Link';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import MenuSeparator from '../ui/MenuSeparator';
|
||||
import ProgressSpinner from '../ui/ProgressSpinner';
|
||||
import AnimatedFileSize from './AnimatedFileSize';
|
||||
import AnimatedIcon from './AnimatedIcon';
|
||||
@ -79,6 +84,7 @@ type OwnProps = {
|
||||
onReadMedia?: () => void;
|
||||
onCancelUpload?: () => void;
|
||||
onDateClick?: (arg: ApiMessage) => void;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
};
|
||||
|
||||
type StateProps = {
|
||||
@ -119,6 +125,7 @@ const Audio = ({
|
||||
onReadMedia,
|
||||
onCancelUpload,
|
||||
onDateClick,
|
||||
contextActions,
|
||||
}: OwnProps & StateProps) => {
|
||||
const {
|
||||
cancelMediaDownload, downloadMedia, transcribeAudio, openOneTimeMediaModal,
|
||||
@ -133,6 +140,8 @@ const Audio = ({
|
||||
const media = (voice || video || audio)!;
|
||||
const mediaSource = (voice || video);
|
||||
const isVoice = Boolean(voice || video);
|
||||
const containerRef = useRef<HTMLDivElement>();
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
const isSeekingRef = useRef<boolean>(false);
|
||||
const seekerRef = useRef<HTMLDivElement>();
|
||||
const oldLang = useOldLang();
|
||||
@ -265,6 +274,17 @@ const Audio = ({
|
||||
}
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(containerRef, !contextActions);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => containerRef.current);
|
||||
const getRootElement = useLastCallback(() => containerRef.current!.closest('.custom-scroll') || document.body);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const handleSeek = useLastCallback((e: MouseEvent | TouchEvent) => {
|
||||
if (isSeekingRef.current && seekerRef.current) {
|
||||
const { width, left } = seekerRef.current.getBoundingClientRect();
|
||||
@ -343,6 +363,7 @@ const Audio = ({
|
||||
isOwn && origin === AudioOrigin.Inline && 'own',
|
||||
(origin === AudioOrigin.Search || origin === AudioOrigin.SharedMedia) && 'bigger',
|
||||
isSelected && 'audio-is-selected',
|
||||
contextMenuAnchor && 'has-menu-open',
|
||||
);
|
||||
|
||||
const buttonClassNames = ['toogle-play-wrapper'];
|
||||
@ -420,7 +441,13 @@ const Audio = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div className={fullClassName} dir={lang.isRtl ? 'rtl' : 'ltr'}>
|
||||
<div
|
||||
ref={containerRef}
|
||||
className={fullClassName}
|
||||
dir={lang.isRtl ? 'rtl' : 'ltr'}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={contextActions ? handleContextMenu : undefined}
|
||||
>
|
||||
{isSelectable && (
|
||||
<div className="message-select-control no-selection">
|
||||
{isSelected && <Icon name="check" className="message-select-control-icon" />}
|
||||
@ -492,6 +519,38 @@ const Audio = ({
|
||||
origin,
|
||||
)
|
||||
)}
|
||||
{contextActions && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="shared-media-context-menu"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
withPortal
|
||||
>
|
||||
{contextActions.map((action) => (
|
||||
('isSeparator' in action) ? (
|
||||
<MenuSeparator key={action.key || 'separator'} />
|
||||
) : (
|
||||
<MenuItem
|
||||
key={action.title}
|
||||
icon={action.icon}
|
||||
destructive={action.destructive}
|
||||
disabled={!action.handler}
|
||||
onClick={action.handler}
|
||||
>
|
||||
{action.title}
|
||||
</MenuItem>
|
||||
)
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -5,6 +5,7 @@ import { getActions } from '../../global';
|
||||
|
||||
import type { ApiDocument, ApiMessage, MediaContent } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { MenuItemContextAction } from '../ui/ListItem';
|
||||
|
||||
import {
|
||||
getDocumentMediaHash,
|
||||
@ -42,6 +43,7 @@ type OwnProps = {
|
||||
shouldWarnAboutFiles?: boolean;
|
||||
id?: string;
|
||||
onCancelUpload?: NoneToVoidFunction;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
} & ({
|
||||
message: ApiMessage;
|
||||
onDateClick: (arg: ApiMessage) => void;
|
||||
@ -73,12 +75,13 @@ const Document = ({
|
||||
onCancelUpload,
|
||||
onMediaClick,
|
||||
onDateClick,
|
||||
contextActions,
|
||||
}: OwnProps) => {
|
||||
const { cancelMediaDownload, downloadMedia, setSharedSettingOption } = getActions();
|
||||
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
|
||||
const lang = useOldLang();
|
||||
const oldLang = useOldLang();
|
||||
const [isFileIpDialogOpen, openFileIpDialog, closeFileIpDialog] = useFlag();
|
||||
const [shouldNotWarnAboutFiles, setShouldNotWarnAboutFiles] = useState(false);
|
||||
|
||||
@ -209,6 +212,7 @@ const Document = ({
|
||||
isSelectable={isSelectable}
|
||||
isSelected={isSelected}
|
||||
actionIcon={withMediaViewer ? (isDocumentVideo(document) ? 'play' : 'eye') : 'download'}
|
||||
contextActions={contextActions}
|
||||
onClick={handleClick}
|
||||
onDateClick={onDateClick ? handleDateClick : undefined}
|
||||
/>
|
||||
@ -217,11 +221,11 @@ const Document = ({
|
||||
onClose={closeFileIpDialog}
|
||||
confirmHandler={handleFileIpConfirm}
|
||||
>
|
||||
{lang('lng_launch_svg_warning')}
|
||||
{oldLang('lng_launch_svg_warning')}
|
||||
<Checkbox
|
||||
className="dialog-checkbox"
|
||||
checked={shouldNotWarnAboutFiles}
|
||||
label={lang('lng_launch_exe_dont_ask')}
|
||||
label={oldLang('lng_launch_exe_dont_ask')}
|
||||
onCheck={setShouldNotWarnAboutFiles}
|
||||
/>
|
||||
</ConfirmDialog>
|
||||
|
||||
@ -1,6 +1,7 @@
|
||||
.File {
|
||||
--secondary-color: var(--color-text-secondary);
|
||||
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
@ -140,6 +141,12 @@
|
||||
}
|
||||
}
|
||||
|
||||
&.has-menu-open {
|
||||
border-radius: var(--border-radius-default);
|
||||
background-color: var(--color-chat-hover);
|
||||
box-shadow: 0 0 0 0.5rem var(--color-chat-hover);
|
||||
}
|
||||
|
||||
.file-info {
|
||||
overflow: hidden;
|
||||
flex-grow: 1;
|
||||
|
||||
@ -6,6 +6,7 @@ import {
|
||||
import type { ApiAttachment, MediaContent } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { IconName } from '../../types/icons';
|
||||
import type { MenuItemContextAction } from '../ui/ListItem';
|
||||
|
||||
import buildClassName from '../../util/buildClassName';
|
||||
import { formatMediaDateTime, formatPastTimeShort } from '../../util/dates/oldDateFormat';
|
||||
@ -13,11 +14,16 @@ import { getColorFromExtension } from './helpers/documentInfo';
|
||||
import { getDocumentThumbnailDimensions } from './helpers/mediaDimensions';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
import useShowTransitionDeprecated from '../../hooks/useShowTransitionDeprecated';
|
||||
|
||||
import Link from '../ui/Link';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import MenuSeparator from '../ui/MenuSeparator';
|
||||
import ProgressSpinner from '../ui/ProgressSpinner';
|
||||
import AnimatedFileSize from './AnimatedFileSize';
|
||||
import CompactMediaPreview, { canRenderCompactMediaPreview } from './CompactMediaPreview';
|
||||
@ -46,6 +52,7 @@ type OwnProps = {
|
||||
isSelected?: boolean;
|
||||
transferProgress?: number;
|
||||
actionIcon?: IconName;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
onClick?: () => void;
|
||||
onDateClick?: (e: React.MouseEvent<HTMLAnchorElement>) => void;
|
||||
};
|
||||
@ -68,6 +75,7 @@ const File = ({
|
||||
isSelected,
|
||||
transferProgress,
|
||||
actionIcon,
|
||||
contextActions,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
onDateClick,
|
||||
@ -78,6 +86,7 @@ const File = ({
|
||||
if (ref) {
|
||||
elementRef = ref;
|
||||
}
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
|
||||
const {
|
||||
shouldRender: shouldSpinnerRender,
|
||||
@ -86,6 +95,17 @@ const File = ({
|
||||
|
||||
const color = getColorFromExtension(extension);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(elementRef, !contextActions);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => elementRef.current);
|
||||
const getRootElement = useLastCallback(() => elementRef.current!.closest('.custom-scroll') || document.body);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const { width } = getDocumentThumbnailDimensions(previewSize);
|
||||
const shouldRenderPreview = canRenderCompactMediaPreview(previewMedia, previewAttachment);
|
||||
|
||||
@ -95,10 +115,18 @@ const File = ({
|
||||
previewSize !== 'medium' && `size-${previewSize}`,
|
||||
onClick && !isUploading && 'interactive',
|
||||
isSelected && 'file-is-selected',
|
||||
contextMenuAnchor && 'has-menu-open',
|
||||
);
|
||||
|
||||
return (
|
||||
<div id={id} ref={elementRef} className={fullClassName} dir={lang.isRtl ? 'rtl' : undefined}>
|
||||
<div
|
||||
id={id}
|
||||
ref={elementRef}
|
||||
className={fullClassName}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={contextActions ? handleContextMenu : undefined}
|
||||
>
|
||||
{isSelectable && (
|
||||
<div className="message-select-control no-selection">
|
||||
{isSelected && <Icon name="check" className="message-select-control-icon" />}
|
||||
@ -157,6 +185,38 @@ const File = ({
|
||||
{sender && Boolean(timestamp) && (
|
||||
<Link onClick={onDateClick}>{formatPastTimeShort(oldLang, timestamp * 1000)}</Link>
|
||||
)}
|
||||
{contextActions && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="shared-media-context-menu"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
withPortal
|
||||
>
|
||||
{contextActions.map((action) => (
|
||||
('isSeparator' in action) ? (
|
||||
<MenuSeparator key={action.key || 'separator'} />
|
||||
) : (
|
||||
<MenuItem
|
||||
key={action.title}
|
||||
icon={action.icon}
|
||||
destructive={action.destructive}
|
||||
disabled={!action.handler}
|
||||
onClick={action.handler}
|
||||
>
|
||||
{action.title}
|
||||
</MenuItem>
|
||||
)
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -8,6 +8,25 @@
|
||||
height: 0;
|
||||
padding-bottom: 100%;
|
||||
|
||||
&::after {
|
||||
pointer-events: none;
|
||||
content: "";
|
||||
|
||||
position: absolute;
|
||||
z-index: 2;
|
||||
inset: 0;
|
||||
|
||||
opacity: 0;
|
||||
background: rgba(0, 0, 0, 0.16);
|
||||
box-shadow: inset 0 0 0 0.125rem var(--color-primary);
|
||||
|
||||
transition: opacity 0.15s ease-in-out;
|
||||
}
|
||||
|
||||
&.has-menu-open::after {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.video-duration {
|
||||
position: absolute;
|
||||
top: 0.3125rem;
|
||||
|
||||
@ -2,6 +2,7 @@ import { memo, useRef } from '../../lib/teact/teact';
|
||||
|
||||
import type { ApiMessage } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { MenuItemContextAction } from '../ui/ListItem';
|
||||
|
||||
import {
|
||||
getMessageHtmlId,
|
||||
@ -16,12 +17,16 @@ import stopEvent from '../../util/stopEvent';
|
||||
|
||||
import useMessageMediaHash from '../../hooks/media/useMessageMediaHash';
|
||||
import useThumbnail from '../../hooks/media/useThumbnail';
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useFlag from '../../hooks/useFlag';
|
||||
import { useIsIntersecting } from '../../hooks/useIntersectionObserver';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useMedia from '../../hooks/useMedia';
|
||||
import useMediaTransitionDeprecated from '../../hooks/useMediaTransitionDeprecated';
|
||||
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import MenuSeparator from '../ui/MenuSeparator';
|
||||
import OptimizedVideo from '../ui/OptimizedVideo';
|
||||
import MediaSpoiler from './MediaSpoiler';
|
||||
|
||||
@ -34,6 +39,7 @@ type OwnProps = {
|
||||
canAutoPlay?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
onClick?: (messageId: number, chatId: string) => void;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
};
|
||||
|
||||
const Media = ({
|
||||
@ -43,8 +49,10 @@ const Media = ({
|
||||
canAutoPlay,
|
||||
observeIntersection,
|
||||
onClick,
|
||||
contextActions,
|
||||
}: OwnProps) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
|
||||
const isIntersecting = useIsIntersecting(ref, observeIntersection);
|
||||
const [isHovering, markMouseOver, markMouseOut] = useFlag();
|
||||
@ -61,7 +69,20 @@ const Media = ({
|
||||
const hasSpoiler = getMessageIsSpoiler(message);
|
||||
const [isSpoilerShown, , hideSpoiler] = useFlag(hasSpoiler);
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, !contextActions);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll') || document.body);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
const handleClick = useLastCallback(() => {
|
||||
if (isContextMenuOpen) return;
|
||||
|
||||
hideSpoiler();
|
||||
onClick!(message.id, message.chatId);
|
||||
});
|
||||
@ -70,10 +91,12 @@ const Media = ({
|
||||
<div
|
||||
ref={ref}
|
||||
id={`${idPrefix}${getMessageHtmlId(message.id)}`}
|
||||
className="Media scroll-item"
|
||||
className={buildClassName('Media scroll-item', contextMenuAnchor && 'has-menu-open')}
|
||||
onClick={onClick ? handleClick : undefined}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onMouseOver={!IS_TOUCH_ENV ? markMouseOver : undefined}
|
||||
onMouseOut={!IS_TOUCH_ENV ? markMouseOut : undefined}
|
||||
onContextMenu={contextActions ? handleContextMenu : undefined}
|
||||
>
|
||||
<img
|
||||
src={thumbDataUri}
|
||||
@ -81,7 +104,7 @@ const Media = ({
|
||||
alt=""
|
||||
draggable={!isProtected}
|
||||
decoding="async"
|
||||
onContextMenu={isProtected ? stopEvent : undefined}
|
||||
onContextMenu={isProtected && !contextActions ? stopEvent : undefined}
|
||||
/>
|
||||
{fullGifBlobUrl ? (
|
||||
<OptimizedVideo
|
||||
@ -93,7 +116,7 @@ const Media = ({
|
||||
playsInline
|
||||
draggable={false}
|
||||
disablePictureInPicture
|
||||
onContextMenu={isProtected ? stopEvent : undefined}
|
||||
onContextMenu={isProtected && !contextActions ? stopEvent : undefined}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
@ -102,7 +125,7 @@ const Media = ({
|
||||
alt=""
|
||||
draggable={false}
|
||||
decoding="async"
|
||||
onContextMenu={isProtected ? stopEvent : undefined}
|
||||
onContextMenu={isProtected && !contextActions ? stopEvent : undefined}
|
||||
/>
|
||||
)}
|
||||
{hasSpoiler && (
|
||||
@ -114,6 +137,38 @@ const Media = ({
|
||||
)}
|
||||
{video && <span className="video-duration">{video.isGif ? 'GIF' : formatMediaDuration(video.duration)}</span>}
|
||||
{isProtected && <span className="protector" />}
|
||||
{contextActions && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="shared-media-context-menu"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
withPortal
|
||||
>
|
||||
{contextActions.map((action) => (
|
||||
('isSeparator' in action) ? (
|
||||
<MenuSeparator key={action.key || 'separator'} />
|
||||
) : (
|
||||
<MenuItem
|
||||
key={action.title}
|
||||
icon={action.icon}
|
||||
destructive={action.destructive}
|
||||
disabled={!action.handler}
|
||||
onClick={action.handler}
|
||||
>
|
||||
{action.title}
|
||||
</MenuItem>
|
||||
)
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -11,6 +11,12 @@
|
||||
margin-top: 1.5rem;
|
||||
}
|
||||
|
||||
&.has-menu-open {
|
||||
border-radius: var(--border-radius-default);
|
||||
background-color: var(--color-chat-hover);
|
||||
box-shadow: 0 0 0 0.5rem var(--color-chat-hover);
|
||||
}
|
||||
|
||||
&.without-media::before {
|
||||
content: attr(data-initial);
|
||||
|
||||
|
||||
@ -1,9 +1,10 @@
|
||||
import { memo, useMemo } from '../../lib/teact/teact';
|
||||
import { memo, useMemo, useRef } from '../../lib/teact/teact';
|
||||
import { withGlobal } from '../../global';
|
||||
|
||||
import type { ApiMessage, ApiWebPage } from '../../api/types';
|
||||
import type { ObserveFn } from '../../hooks/useIntersectionObserver';
|
||||
import type { TextPart } from '../../types';
|
||||
import type { MenuItemContextAction } from '../ui/ListItem';
|
||||
|
||||
import {
|
||||
getFirstLinkInMessage,
|
||||
@ -16,11 +17,15 @@ import trimText from '../../util/trimText';
|
||||
import { renderMessageSummary } from './helpers/renderMessageText';
|
||||
import renderText from './helpers/renderText';
|
||||
|
||||
import useContextMenuHandlers from '../../hooks/useContextMenuHandlers';
|
||||
import useLang from '../../hooks/useLang';
|
||||
import useLastCallback from '../../hooks/useLastCallback';
|
||||
import useOldLang from '../../hooks/useOldLang';
|
||||
|
||||
import Link from '../ui/Link';
|
||||
import Menu from '../ui/Menu';
|
||||
import MenuItem from '../ui/MenuItem';
|
||||
import MenuSeparator from '../ui/MenuSeparator';
|
||||
import Media from './Media';
|
||||
import SafeLink from './SafeLink';
|
||||
|
||||
@ -37,6 +42,7 @@ type OwnProps = {
|
||||
senderTitle?: string;
|
||||
isProtected?: boolean;
|
||||
observeIntersection?: ObserveFn;
|
||||
contextActions?: MenuItemContextAction[];
|
||||
onMessageClick: (message: ApiMessage) => void;
|
||||
};
|
||||
|
||||
@ -45,8 +51,10 @@ type StateProps = {
|
||||
};
|
||||
|
||||
const WebLink = ({
|
||||
message, webPage, senderTitle, isProtected, observeIntersection, onMessageClick,
|
||||
message, webPage, senderTitle, isProtected, observeIntersection, contextActions, onMessageClick,
|
||||
}: OwnProps & StateProps) => {
|
||||
const ref = useRef<HTMLDivElement>();
|
||||
const menuRef = useRef<HTMLDivElement>();
|
||||
const lang = useLang();
|
||||
const oldLang = useOldLang();
|
||||
|
||||
@ -54,6 +62,17 @@ const WebLink = ({
|
||||
onMessageClick(message);
|
||||
});
|
||||
|
||||
const {
|
||||
isContextMenuOpen, contextMenuAnchor,
|
||||
handleBeforeContextMenu, handleContextMenu,
|
||||
handleContextMenuClose, handleContextMenuHide,
|
||||
} = useContextMenuHandlers(ref, !contextActions, true);
|
||||
|
||||
const getTriggerElement = useLastCallback(() => ref.current);
|
||||
const getRootElement = useLastCallback(() => ref.current!.closest('.custom-scroll') || document.body);
|
||||
const getMenuElement = useLastCallback(() => menuRef.current);
|
||||
const getLayout = useLastCallback(() => ({ withPortal: true }));
|
||||
|
||||
let linkData: ApiWebPageWithFormatted | undefined = webPage;
|
||||
|
||||
if (!linkData) {
|
||||
@ -114,18 +133,27 @@ const WebLink = ({
|
||||
const className = buildClassName(
|
||||
'WebLink scroll-item',
|
||||
(!photo && !video) && 'without-media',
|
||||
contextMenuAnchor && 'has-menu-open',
|
||||
);
|
||||
|
||||
const safeLinkContent = displayUrl || url.replace('mailto:', '');
|
||||
|
||||
return (
|
||||
<div
|
||||
ref={ref}
|
||||
className={className}
|
||||
data-initial={siteTitle[0]}
|
||||
dir={lang.isRtl ? 'rtl' : undefined}
|
||||
onMouseDown={handleBeforeContextMenu}
|
||||
onContextMenu={contextActions ? handleContextMenu : undefined}
|
||||
>
|
||||
{photo && (
|
||||
<Media message={message} isProtected={isProtected} observeIntersection={observeIntersection} />
|
||||
<Media
|
||||
message={message}
|
||||
isProtected={isProtected}
|
||||
observeIntersection={observeIntersection}
|
||||
contextActions={contextActions}
|
||||
/>
|
||||
)}
|
||||
<div className="content">
|
||||
<Link isRtl={lang.isRtl} className="site-title" onClick={handleMessageClick}>
|
||||
@ -155,6 +183,38 @@ const WebLink = ({
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
{contextActions && contextMenuAnchor !== undefined && (
|
||||
<Menu
|
||||
ref={menuRef}
|
||||
isOpen={isContextMenuOpen}
|
||||
anchor={contextMenuAnchor}
|
||||
getTriggerElement={getTriggerElement}
|
||||
getRootElement={getRootElement}
|
||||
getMenuElement={getMenuElement}
|
||||
getLayout={getLayout}
|
||||
className="shared-media-context-menu"
|
||||
autoClose
|
||||
onClose={handleContextMenuClose}
|
||||
onCloseAnimationEnd={handleContextMenuHide}
|
||||
withPortal
|
||||
>
|
||||
{contextActions.map((action) => (
|
||||
('isSeparator' in action) ? (
|
||||
<MenuSeparator key={action.key || 'separator'} />
|
||||
) : (
|
||||
<MenuItem
|
||||
key={action.title}
|
||||
icon={action.icon}
|
||||
destructive={action.destructive}
|
||||
disabled={!action.handler}
|
||||
onClick={action.handler}
|
||||
>
|
||||
{action.title}
|
||||
</MenuItem>
|
||||
)
|
||||
))}
|
||||
</Menu>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@ -710,6 +710,16 @@ const Profile = ({
|
||||
focusMessage({ chatId: message.chatId, messageId: message.id });
|
||||
});
|
||||
|
||||
const getMessageContextActions = useLastCallback((message: ApiMessage): MenuItemContextAction[] => {
|
||||
return [{
|
||||
title: lang('FocusMessage'),
|
||||
icon: 'show-message',
|
||||
handler: () => {
|
||||
handleMessageFocus(message);
|
||||
},
|
||||
}];
|
||||
});
|
||||
|
||||
const handleDeleteMembersModalClose = useLastCallback(() => {
|
||||
setDeletingUserId(undefined);
|
||||
});
|
||||
@ -1023,6 +1033,7 @@ const Profile = ({
|
||||
canAutoPlay={canAutoPlayGifs}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
onClick={handleSelectMedia}
|
||||
contextActions={getMessageContextActions(messagesById[id])}
|
||||
/>
|
||||
))
|
||||
) : (resultType === 'stories' || resultType === 'storiesArchive') ? (
|
||||
@ -1049,6 +1060,7 @@ const Profile = ({
|
||||
message={messagesById[id]}
|
||||
shouldWarnAboutFiles={shouldWarnAboutFiles}
|
||||
onMediaClick={handleSelectMedia}
|
||||
contextActions={getMessageContextActions(messagesById[id])}
|
||||
/>
|
||||
))
|
||||
) : resultType === 'links' ? (
|
||||
@ -1058,6 +1070,7 @@ const Profile = ({
|
||||
message={messagesById[id]}
|
||||
isProtected={isChatProtected || messagesById[id].isProtected}
|
||||
observeIntersection={observeIntersectionForMedia}
|
||||
contextActions={getMessageContextActions(messagesById[id])}
|
||||
onMessageClick={handleMessageFocus}
|
||||
/>
|
||||
))
|
||||
@ -1072,6 +1085,7 @@ const Profile = ({
|
||||
className="scroll-item"
|
||||
onPlay={handlePlayAudio}
|
||||
onDateClick={handleMessageFocus}
|
||||
contextActions={getMessageContextActions(messagesById[id])}
|
||||
canDownload={!isChatProtected && !messagesById[id].isProtected}
|
||||
isDownloading={getIsDownloading(activeDownloads, messagesById[id].content.audio!)}
|
||||
/>
|
||||
@ -1093,6 +1107,7 @@ const Profile = ({
|
||||
className="scroll-item"
|
||||
onPlay={handlePlayAudio}
|
||||
onDateClick={handleMessageFocus}
|
||||
contextActions={getMessageContextActions(message)}
|
||||
canDownload={!isChatProtected && !message.isProtected}
|
||||
isDownloading={getIsDownloading(activeDownloads, media)}
|
||||
/>
|
||||
|
||||
@ -18,6 +18,16 @@ function stopEvent(e: Event) {
|
||||
e.stopPropagation();
|
||||
}
|
||||
|
||||
function isNativeLinkTarget(target: EventTarget | undefined) {
|
||||
if (!(target instanceof HTMLElement)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const link = target.closest('a[href]');
|
||||
|
||||
return Boolean(link && link.getAttribute('href') !== '#');
|
||||
}
|
||||
|
||||
const useContextMenuHandlers = (
|
||||
elementRef: ElementRef<HTMLElement>,
|
||||
isMenuDisabled?: boolean,
|
||||
@ -43,7 +53,7 @@ const useContextMenuHandlers = (
|
||||
removeExtraClass(e.target as HTMLElement, 'no-selection');
|
||||
});
|
||||
|
||||
if (isMenuDisabled || (shouldDisableOnLink && (e.target as HTMLElement).matches('a[href]'))) {
|
||||
if (isMenuDisabled || (shouldDisableOnLink && isNativeLinkTarget(e.target))) {
|
||||
return;
|
||||
}
|
||||
e.preventDefault();
|
||||
@ -91,7 +101,7 @@ const useContextMenuHandlers = (
|
||||
|
||||
const { clientX, clientY, target } = originalEvent.touches[0];
|
||||
|
||||
if (contextMenuAnchor || (shouldDisableOnLink && (target as HTMLElement).matches('a[href]'))) {
|
||||
if (contextMenuAnchor || (shouldDisableOnLink && isNativeLinkTarget(target))) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user