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';
import {SUPPRESS_EMBEDS_DESCRIPTOR} from '@app/features/i18n/utils/CommonMessageDescriptors';
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 {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
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);
}
};
const openInBrowser = useOpenInBrowserOnMiddleClick(url);
return (
<FocusRing data-flx="channel.embeds.embed.link-component.focus-ring">
<a
@@ -39,6 +41,7 @@ export const EmbedLink: FC<LinkComponentProps> = observer(({url, children, class
rel="noopener noreferrer"
target="_blank"
onClick={handleClick}
onAuxClick={openInBrowser.onAuxClick}
data-flx="channel.embeds.embed.link-component.a"
>
{children}
@@ -31,8 +31,11 @@ import {
type RendererProps,
} from '@app/features/messaging/components/markdown/renderers/RendererTypes';
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 {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
import {goToMessage} from '@app/features/messaging/utils/MessageNavigator';
import type {LinkNode} from '@app/features/messaging/utils/markdown/parser/Nodes';
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 & {
'--jump-link-guild-icon-image'?: string;
};
@@ -324,16 +340,7 @@ const JumpLinkMention = observer(function JumpLinkMention({
},
[channel, guild?.id, interactive, navigateToJumpTarget],
);
const handleAuxClick = useCallback(
(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 handleAuxClick = useJumpLinkAuxClick(url, interactive);
const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
@@ -469,16 +476,7 @@ function InaccessibleJumpLinkMention({url, i18n, interactive = true}: Inaccessib
},
[i18n, interactive],
);
const handleAuxClick = useCallback(
(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 handleAuxClick = useJumpLinkAuxClick(url, interactive);
const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
@@ -555,16 +553,7 @@ const DiscoverableJumpLinkMention = observer(function DiscoverableJumpLinkMentio
},
[channelId, guildId, interactive, joining, messageId],
);
const handleAuxClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
void openExternalUrl(url);
},
[interactive, url],
);
const handleAuxClick = useJumpLinkAuxClick(url, interactive);
const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
@@ -705,16 +694,7 @@ function SettingsJumpLinkMention({target, url, i18n, interactive = true}: Settin
},
[interactive, url],
);
const handleAuxClick = useCallback(
(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 handleAuxClick = useJumpLinkAuxClick(url, interactive);
const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
@@ -808,16 +788,7 @@ function AppPageJumpLinkMention({page, url, i18n, interactive = true}: AppPageJu
},
[interactive, page],
);
const handleAuxClick = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
if (event.button !== 1) return;
event.preventDefault();
event.stopPropagation();
void openExternalUrl(url);
},
[interactive, url],
);
const handleAuxClick = useJumpLinkAuxClick(url, interactive);
const handleContextMenu = useCallback(
(event: React.MouseEvent<HTMLButtonElement | HTMLSpanElement>) => {
if (!interactive) return;
@@ -876,6 +847,7 @@ export const LinkRenderer = observer(function LinkRenderer({
}: RendererProps<LinkNode>): React.ReactElement {
const i18n = options.i18n!;
const {url, text} = node;
const openInBrowser = useOpenInBrowserOnMiddleClick(url);
const content = text ? renderChildren([text], LINK_CONTENT_OPTION_OVERRIDES) : url;
const inviteCode = InviteUtils.findInvite(url);
const themeCode = ThemeUtils.findTheme(url);
@@ -1199,14 +1171,7 @@ export const LinkRenderer = observer(function LinkRenderer({
void openExternalUrl(url);
}
}}
onAuxClick={(e) => {
if (e.button !== 1 || isInternal) {
return;
}
e.preventDefault();
e.stopPropagation();
openExternalUrlWithWarning(url);
}}
onAuxClick={isInternal ? undefined : openInBrowser.onAuxClick}
className={markupStyles.link}
data-flx="messaging.markdown.renderers.link-renderer.a"
>
@@ -1,10 +1,10 @@
// 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 {useMemo} from 'react';
const MIDDLE_MOUSE_BUTTON = 1;
export const MIDDLE_MOUSE_BUTTON = 1;
interface MiddleClickOpenHandlers {
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;
event.preventDefault();
event.stopPropagation();
void openExternalUrl(url);
openExternalUrlWithWarning(url);
},
}),
[url, enabled],