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