Cleanup - unused files / unused exports / duplicate exports
[ProtonMail-WebClient.git] / packages / components / hooks / useLinkHandler.tsx
blob6057197b64a2f66c48b2c88ee139c0a9e709c065
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;
23     isOutside?: 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 = {
33     startListening: true,
36 const getSrc = (target: Element) => {
37     const extract = () => {
38         try {
39             return { encoded: target.toString() || '', raw: target.getAttribute('href') || '' };
40         } catch (e: any) {
41             /*
42                 Because for Edge/IE11
43                 <a href="http://xn--rotonmail-4sg.com" rel="noreferrer nofollow noopener">Protonmail.com</a>
44                 will crash --> Unspecified error. ¯\_(ツ)_/¯
45                 Don't worry, target.href/getAttribute will crash too ¯\_(ツ)_/¯
46              */
47             const attr = Array.from(target.attributes).find((attr) => (attr || {}).name === 'href');
48             return { raw: attr?.nodeValue || '' };
49         }
50     };
52     // Because even the fallback can crash on IE11/Edge
53     try {
54         return extract();
55     } catch (e: any) {
56         return { raw: '' };
57     }
60 export const useLinkHandler: UseLinkHandler = (
61     wrapperRef,
62     mailSettings,
63     { onMailTo, startListening, isOutside, isPhishingAttempt } = defaultOptions
64 ) => {
65     const { createNotification } = useNotifications();
66     const [link, setLink] = useState<string>();
67     const [linkConfirmationModalProps, setLinkConfirmationModalOpen, renderLinkConfirmationModal] = useModalState();
69     // eslint-disable-next-line @typescript-eslint/no-misused-promises
70     const handleClick = useHandler(async (event: Event) => {
71         const originalTarget = event.target as Element;
72         const target = originalTarget.closest('a') || originalTarget.closest('area');
74         if (!target) {
75             return;
76         }
78         const src = getSrc(target);
80         if (!src.raw) {
81             createNotification({
82                 text: c('Error')
83                     .t`This message may contain some links URL that cannot be properly opened by your current browser.`,
84                 type: 'error',
85             });
86         }
88         // IE11 and Edge random env bug... (╯°□°)╯︵ ┻━┻
89         if (!src) {
90             event.preventDefault();
91             return false;
92         }
94         // We only handle anchor that begins with `mailto:`
95         if (src.raw.toLowerCase().startsWith('mailto:') && onMailTo) {
96             event.preventDefault();
97             event.stopPropagation(); // Required for Safari
99             /*
100              * Open the composer with the given mailto address
101              * position isAfter true as the user can choose to set a body
102              */
103             onMailTo(src.raw);
104         }
106         const askForConfirmation = mailSettings?.ConfirmLink ?? CONFIRM_LINK.CONFIRM;
107         const hostname = getHostname(src.raw);
108         const currentDomain = getSecondLevelDomain(window.location.hostname);
110         /*
111          * If the modal is already active --- do nothing
112          * ex: click on a link, open the modal, inside the continue button is an anchor with the same link.
113          */
114         if (linkConfirmationModalProps.open) {
115             return;
116         }
118         /*
119          * If dealing with anchors, we need to treat them separately because we use URLs with # for searching elements
120          */
121         if (src.raw.startsWith('#')) {
122             const id = src.raw.replace('#', '');
123             if (wrapperRef.current) {
124                 const elementInMail = wrapperRef.current.querySelector(`[name="${id}"], [id="${id}"]`);
125                 if (elementInMail) {
126                     elementInMail.scrollIntoView({ behavior: 'smooth', block: 'start' });
127                 }
128             }
129             event.preventDefault();
130             event.stopPropagation();
131             return;
132         }
134         if (
135             (askForConfirmation || isPhishingAttempt) &&
136             isExternal(src.raw, window.location.hostname) &&
137             ![...PROTON_DOMAINS, currentDomain]
138                 .filter(isTruthy) // currentDomain can be null
139                 .some((domain) => isSubDomain(hostname, domain))
140         ) {
141             event.preventDefault();
142             event.stopPropagation(); // Required for Safari
144             const link = punycodeUrl(src.encoded || src.raw);
145             setLink(link);
146             setLinkConfirmationModalOpen(true);
147         }
148     });
150     useEffect(() => {
151         if (startListening === false) {
152             return;
153         }
155         // eslint-disable-next-line @typescript-eslint/no-misused-promises
156         wrapperRef.current?.addEventListener('click', handleClick, false);
157         return () => {
158             // eslint-disable-next-line @typescript-eslint/no-misused-promises
159             wrapperRef.current?.removeEventListener('click', handleClick, false);
160         };
161     }, [startListening, wrapperRef.current]);
163     const modal = renderLinkConfirmationModal ? (
164         <LinkConfirmationModal
165             link={link}
166             isOutside={isOutside}
167             isPhishingAttempt={isPhishingAttempt}
168             modalProps={linkConfirmationModalProps}
169         />
170     ) : null;
172     return { modal };