fix(links): warn on embed link middle clicks (#3305)

This commit is contained in:
Jiralite authored and GitHub committed 2026-10-08 20:05:27 +02:00
1 parent abfaff16c2
commit ebf91ccc76
3 files changed
+30 -62

No files matched your search

@@ -8,6 +8,7 @@ import {
} from '@app/features/channel/components/embeds/channel_embed/ChannelEmbedShared'; } from '@app/features/channel/components/embeds/channel_embed/ChannelEmbedShared';
import {SUPPRESS_EMBEDS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors'; import {SUPPRESS_EMBEDS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
import * as MessageCommands from '@app/features/messaging/commands/MessageCommands'; import * as MessageCommands from '@app/features/messaging/commands/MessageCommands';
import {useOpenInBrowserOnMiddleClick} from '@app/features/messaging/hooks/useOpenInBrowserOnMiddleClick';
import type {Message} from '@app/features/messaging/models/MessagingMessage'; import type {Message} from '@app/features/messaging/models/MessagingMessage';
import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils'; import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
import TrustedDomain from '@app/features/trusted_domain/state/TrustedDomain'; import TrustedDomain from '@app/features/trusted_domain/state/TrustedDomain';
@@ -31,6 +32,7 @@ export const EmbedLink: FC<LinkComponentProps> = observer(({url, children, class
logger.warn('Invalid URL in embed link:', url); logger.warn('Invalid URL in embed link:', url);
} }
}; };
const openInBrowser = useOpenInBrowserOnMiddleClick(url);
return ( return (
<FocusRing data-flx="channel.embeds.embed.link-component.focus-ring"> <FocusRing data-flx="channel.embeds.embed.link-component.focus-ring">
<a <a
@@ -39,6 +41,7 @@ export const EmbedLink: FC<LinkComponentProps> = observer(({url, children, class
rel="noopener noreferrer" rel="noopener noreferrer"
target="_blank" target="_blank"
onClick={handleClick} onClick={handleClick}
onAuxClick={openInBrowser.onAuxClick}
data-flx="channel.embeds.embed.link-component.a" data-flx="channel.embeds.embed.link-component.a"
> >
{children} {children}
@@ -31,8 +31,11 @@ import {
type RendererProps, type RendererProps,
} from '@app/features/messaging/components/markdown/renderers/RendererTypes'; } from '@app/features/messaging/components/markdown/renderers/RendererTypes';
import {ExternalLinkWarningModal} from '@app/features/messaging/components/modals/ExternalLinkWarningModal'; import {ExternalLinkWarningModal} from '@app/features/messaging/components/modals/ExternalLinkWarningModal';
import {
MIDDLE_MOUSE_BUTTON,
useOpenInBrowserOnMiddleClick,
} from '@app/features/messaging/hooks/useOpenInBrowserOnMiddleClick';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher'; import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
import {goToMessage} from '@app/features/messaging/utils/MessageNavigator'; import {goToMessage} from '@app/features/messaging/utils/MessageNavigator';
import type {LinkNode} from '@app/features/messaging/utils/markdown/parser/Nodes'; import type {LinkNode} from '@app/features/messaging/utils/markdown/parser/Nodes';
import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands'; import * as NavigationCommands from '@app/features/navigation/commands/NavigationCommands';
@@ -205,6 +208,19 @@ function promptForJumpGate(channel: Channel, scope: string | null | undefined, o
}); });
} }
function useJumpLinkAuxClick(url: string, interactive: boolean) {
return useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== MIDDLE_MOUSE_BUTTON) return;
event.preventDefault();
event.stopPropagation();
if (url) void openExternalUrl(url);
},
[interactive, url],
);
}
type CompactGuildMentionIconStyle = React.CSSProperties & { type CompactGuildMentionIconStyle = React.CSSProperties & {
'--jump-link-guild-icon-image'?: string; '--jump-link-guild-icon-image'?: string;
}; };
@@ -324,16 +340,7 @@ const JumpLinkMention = observer(function JumpLinkMention({
}, },
[channel, guild?.id, interactive, navigateToJumpTarget], [channel, guild?.id, interactive, navigateToJumpTarget],
); );
const handleAuxClick = useCallback( const handleAuxClick = useJumpLinkAuxClick(url, interactive);
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
if (url) void openExternalUrl(url);
},
[interactive, url],
);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => { (event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return; if (!interactive) return;
@@ -469,16 +476,7 @@ function InaccessibleJumpLinkMention({url, i18n, interactive = true}: Inaccessib
}, },
[i18n, interactive], [i18n, interactive],
); );
const handleAuxClick = useCallback( const handleAuxClick = useJumpLinkAuxClick(url, interactive);
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
if (url) void openExternalUrl(url);
},
[interactive, url],
);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => { (event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return; if (!interactive) return;
@@ -555,16 +553,7 @@ const DiscoverableJumpLinkMention = observer(function DiscoverableJumpLinkMentio
}, },
[channelId, guildId, interactive, joining, messageId], [channelId, guildId, interactive, joining, messageId],
); );
const handleAuxClick = useCallback( const handleAuxClick = useJumpLinkAuxClick(url, interactive);
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
void openExternalUrl(url);
},
[interactive, url],
);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => { (event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return; if (!interactive) return;
@@ -705,16 +694,7 @@ function SettingsJumpLinkMention({target, url, i18n, interactive = true}: Settin
}, },
[interactive, url], [interactive, url],
); );
const handleAuxClick = useCallback( const handleAuxClick = useJumpLinkAuxClick(url, interactive);
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
if (url) void openExternalUrl(url);
},
[interactive, url],
);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => { (event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return; if (!interactive) return;
@@ -808,16 +788,7 @@ function AppPageJumpLinkMention({page, url, i18n, interactive = true}: AppPageJu
}, },
[interactive, page], [interactive, page],
); );
const handleAuxClick = useCallback( const handleAuxClick = useJumpLinkAuxClick(url, interactive);
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
void openExternalUrl(url);
},
[interactive, url],
);
const handleContextMenu = useCallback( const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => { (event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return; if (!interactive) return;
@@ -876,6 +847,7 @@ export const LinkRenderer = observer(function LinkRenderer({
}: RendererProps<LinkNode>): React.ReactElement { }: RendererProps<LinkNode>): React.ReactElement {
const i18n = options.i18n!; const i18n = options.i18n!;
const {url, text} = node; const {url, text} = node;
const openInBrowser = useOpenInBrowserOnMiddleClick(url);
const content = text ? renderChildren([text], LINK_CONTENT_OPTION_OVERRIDES) : url; const content = text ? renderChildren([text], LINK_CONTENT_OPTION_OVERRIDES) : url;
const inviteCode = InviteUtils.findInvite(url); const inviteCode = InviteUtils.findInvite(url);
const themeCode = ThemeUtils.findTheme(url); const themeCode = ThemeUtils.findTheme(url);
@@ -1199,14 +1171,7 @@ export const LinkRenderer = observer(function LinkRenderer({
void openExternalUrl(url); void openExternalUrl(url);
} }
}} }}
onAuxClick={(e) => { onAuxClick={isInternal ? undefined : openInBrowser.onAuxClick}
if (e.button !== 1 || isInternal) {
return;
}
e.preventDefault();
e.stopPropagation();
openExternalUrlWithWarning(url);
}}
className={markupStyles.link} className={markupStyles.link}
data-flx="messaging.markdown.renderers.link-renderer.a" data-flx="messaging.markdown.renderers.link-renderer.a"
> >
@@ -1,10 +1,10 @@
// SPDX-License-Identifier: AGPL-3.0-or-later // SPDX-License-Identifier: AGPL-3.0-or-later
import {openExternalUrl} from '@app/features/ui/utils/NativeUtils'; import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
import type React from 'react'; import type React from 'react';
import {useMemo} from 'react'; import {useMemo} from 'react';
const MIDDLE_MOUSE_BUTTON = 1; export const MIDDLE_MOUSE_BUTTON = 1;
interface MiddleClickOpenHandlers { interface MiddleClickOpenHandlers {
onMouseDown: (event: React.MouseEvent) => void; onMouseDown: (event: React.MouseEvent) => void;
@@ -21,7 +21,7 @@ export function useOpenInBrowserOnMiddleClick(url: string | null | undefined, en
if (event.button !== MIDDLE_MOUSE_BUTTON || !enabled || !url) return; if (event.button !== MIDDLE_MOUSE_BUTTON || !enabled || !url) return;
event.preventDefault(); event.preventDefault();
event.stopPropagation(); event.stopPropagation();
void openExternalUrl(url); openExternalUrlWithWarning(url);
}, },
}), }),
[url, enabled], [url, enabled],