1 import type { ReactNode, RefObject } from 'react';
2 import { useEffect, useState } from 'react';
4 import { c } from 'ttag';
6 import useModalState from '@proton/components/components/modalTwo/useModalState';
7 import useNotifications from '@proton/components/hooks/useNotifications';
8 import { PROTON_DOMAINS } from '@proton/shared/lib/constants';
9 import { getSecondLevelDomain, isSubDomain } from '@proton/shared/lib/helpers/url';
10 import type { MailSettings } from '@proton/shared/lib/interfaces';
11 import { CONFIRM_LINK } from '@proton/shared/lib/mail/mailSettings';
12 import isTruthy from '@proton/utils/isTruthy';
14 import LinkConfirmationModal from '../components/notifications/LinkConfirmationModal/LinkConfirmationModal';
15 import { getHostname, isExternal, punycodeUrl } from '../helpers/url';
16 import useHandler from './useHandler';
18 // Reference : Angular/src/app/utils/directives/linkHandler.js
20 interface UseLinkHandlerOptions {
21 onMailTo?: (src: string) => void;
22 startListening?: boolean;
24 isPhishingAttempt?: boolean;
26 type UseLinkHandler = (
27 wrapperRef: RefObject<HTMLDivElement | undefined>,
28 mailSettings?: MailSettings,
29 options?: UseLinkHandlerOptions
30 ) => { modal: ReactNode };
32 const defaultOptions: UseLinkHandlerOptions = {
36 const getSrc = (target: Element) => {
37 return { encoded: target.toString() || '', raw: target.getAttribute('href') || '' };
40 export const useLinkHandler: UseLinkHandler = (
43 { onMailTo, startListening, isOutside, isPhishingAttempt } = defaultOptions
45 const { createNotification } = useNotifications();
46 const [link, setLink] = useState<string>();
47 const [linkConfirmationModalProps, setLinkConfirmationModalOpen, renderLinkConfirmationModal] = useModalState();
49 // eslint-disable-next-line @typescript-eslint/no-misused-promises
50 const handleClick = useHandler(async (event: Event) => {
51 const originalTarget = event.target as Element;
52 const target = originalTarget.closest('a') || originalTarget.closest('area');
58 const src = getSrc(target);
63 .t`This message may contain some links URL that cannot be properly opened by your current browser.`,
68 // We only handle anchor that begins with `mailto:`
69 if (src.raw.toLowerCase().startsWith('mailto:') && onMailTo) {
70 event.preventDefault();
71 event.stopPropagation(); // Required for Safari
74 * Open the composer with the given mailto address
75 * position isAfter true as the user can choose to set a body
80 const askForConfirmation = mailSettings?.ConfirmLink ?? CONFIRM_LINK.CONFIRM;
81 const hostname = getHostname(src.raw);
82 const currentDomain = getSecondLevelDomain(window.location.hostname);
85 * If the modal is already active --- do nothing
86 * ex: click on a link, open the modal, inside the continue button is an anchor with the same link.
88 if (linkConfirmationModalProps.open) {
93 * If dealing with anchors, we need to treat them separately because we use URLs with # for searching elements
95 if (src.raw.startsWith('#')) {
96 const id = src.raw.replace('#', '');
97 if (wrapperRef.current) {
98 const elementInMail = wrapperRef.current.querySelector(`[name="${id}"], [id="${id}"]`);
100 elementInMail.scrollIntoView({ behavior: 'smooth', block: 'start' });
103 event.preventDefault();
104 event.stopPropagation();
109 (askForConfirmation || isPhishingAttempt) &&
110 isExternal(src.raw, window.location.hostname) &&
111 ![...PROTON_DOMAINS, currentDomain]
112 .filter(isTruthy) // currentDomain can be null
113 .some((domain) => isSubDomain(hostname, domain))
115 event.preventDefault();
116 event.stopPropagation(); // Required for Safari
118 const link = punycodeUrl(src.encoded || src.raw);
120 setLinkConfirmationModalOpen(true);
125 if (startListening === false) {
129 // eslint-disable-next-line @typescript-eslint/no-misused-promises
130 wrapperRef.current?.addEventListener('click', handleClick, false);
132 // eslint-disable-next-line @typescript-eslint/no-misused-promises
133 wrapperRef.current?.removeEventListener('click', handleClick, false);
135 }, [startListening, wrapperRef.current]);
137 const modal = renderLinkConfirmationModal ? (
138 <LinkConfirmationModal
140 isOutside={isOutside}
141 isPhishingAttempt={isPhishingAttempt}
142 modalProps={linkConfirmationModalProps}