fix(links): warn on unhandled middle clicks and desktop links (#3311)

This commit is contained in:
Hampus authored and GitHub committed 2026-10-08 20:05:54 +02:00
1 parent ebf91ccc76
commit bef20cba85
9 files changed
+71 -69

No files matched your search

@@ -31,11 +31,9 @@ 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 {useOpenInBrowserOnMiddleClick} from '@app/features/messaging/hooks/useOpenInBrowserOnMiddleClick';
import AttachmentUrlRefresher from '@app/features/messaging/state/AttachmentUrlRefresher';
import {MIDDLE_MOUSE_BUTTON} 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';
@@ -1,27 +1,18 @@
// SPDX-License-Identifier: AGPL-3.0-or-later
import {openExternalUrlWithWarning} from '@app/features/messaging/utils/ExternalLinkUtils';
import {handleExternalLinkAuxClick} from '@app/features/messaging/utils/ExternalLinkUtils';
import type React from 'react';
import {useMemo} from 'react';
export const MIDDLE_MOUSE_BUTTON = 1;
interface MiddleClickOpenHandlers {
onMouseDown: (event: React.MouseEvent) => void;
onAuxClick: (event: React.MouseEvent) => void;
}
export function useOpenInBrowserOnMiddleClick(url: string | null | undefined, enabled = true): MiddleClickOpenHandlers {
return useMemo<MiddleClickOpenHandlers>(
() => ({
onMouseDown: (event) => {
if (event.button !== MIDDLE_MOUSE_BUTTON || !enabled || !url) return;
},
onAuxClick: (event) => {
if (event.button !== MIDDLE_MOUSE_BUTTON || !enabled || !url) return;
event.preventDefault();
event.stopPropagation();
openExternalUrlWithWarning(url);
if (enabled) handleExternalLinkAuxClick(event, url);
},
}),
[url, enabled],
@@ -2,31 +2,79 @@
import {openOAuthAuthorizeModalFromUrl} from '@app/features/auth/commands/OAuthAuthorizeModalCommands';
import {ExternalLinkWarningModal} from '@app/features/messaging/components/modals/ExternalLinkWarningModal';
import {unwrapDesktopLocalResourceURL} from '@app/features/messaging/utils/DesktopLocalResourceTarget';
import TrustedDomain from '@app/features/trusted_domain/state/TrustedDomain';
import * as ModalCommands from '@app/features/ui/commands/ModalCommands';
import {modal} from '@app/features/ui/commands/ModalCommands';
import {openExternalUrl} from '@app/features/ui/utils/NativeUtils';
import {getSafeExternalUrl, isDesktop, openExternalUrl} from '@app/features/ui/utils/NativeUtils';
import type React from 'react';
export function openExternalUrlWithWarning(url: string): void {
if (openOAuthAuthorizeModalFromUrl(url)) {
return;
}
let hostname: string | null = null;
export const MIDDLE_MOUSE_BUTTON = 1;
let externalLinkInterceptorAttached = false;
function isTrustedExternalUrl(url: string): boolean {
try {
hostname = new URL(url).hostname;
return TrustedDomain.isTrustedDomain(new URL(url).hostname);
} catch {
hostname = null;
return false;
}
if (hostname && TrustedDomain.isTrustedDomain(hostname)) {
void openExternalUrl(url);
}
export function openUrlInBrowserWithWarning(url: string): void {
const target = unwrapDesktopLocalResourceURL(url);
if (isTrustedExternalUrl(target)) {
void openExternalUrl(target);
return;
}
ModalCommands.push(
modal(() => (
<ExternalLinkWarningModal
url={url}
url={target}
data-flx="messaging.external-link-utils.open-external-url-with-warning.external-link-warning-modal"
/>
)),
);
}
export function openExternalUrlWithWarning(url: string): void {
if (openOAuthAuthorizeModalFromUrl(url)) {
return;
}
openUrlInBrowserWithWarning(url);
}
export function handleExternalLinkAuxClick(event: React.MouseEvent, url: string | null | undefined): void {
if (event.button !== MIDDLE_MOUSE_BUTTON || !url) return;
event.preventDefault();
event.stopPropagation();
openUrlInBrowserWithWarning(url);
}
function findUnhandledExternalAnchorHref(event: MouseEvent): string | null {
if (event.defaultPrevented) return null;
const target = event.target as Element | null;
const anchor = target?.closest?.('a[target="_blank"]');
const href = anchor?.getAttribute('href') ?? null;
return getSafeExternalUrl(href) ? href : null;
}
export function attachExternalLinkInterceptor(): () => void {
if (!isDesktop() || externalLinkInterceptorAttached) return () => undefined;
const handler = (event: MouseEvent) => {
const expectedButton = event.type === 'click' ? 0 : MIDDLE_MOUSE_BUTTON;
if (event.button !== expectedButton) return;
const href = findUnhandledExternalAnchorHref(event);
if (href == null) return;
event.preventDefault();
openUrlInBrowserWithWarning(href);
};
document.addEventListener('click', handler);
document.addEventListener('auxclick', handler);
externalLinkInterceptorAttached = true;
return () => {
document.removeEventListener('click', handler);
document.removeEventListener('auxclick', handler);
externalLinkInterceptorAttached = false;
};
}