From e83e611caa63bd85d1528f7f4ca0094bb203b148 Mon Sep 17 00:00:00 2001 From: Alexander Zinchuk Date: Fri, 14 May 2021 13:52:41 +0300 Subject: [PATCH] Safe Link Modal: Confirm when opening external links --- src/bundles/extra.ts | 1 + src/components/common/SafeLink.tsx | 33 ++++++++++---- src/components/common/helpers/renderText.tsx | 4 ++ src/components/main/Main.tsx | 5 ++ src/components/main/SafeLinkModal.async.tsx | 16 +++++++ src/components/main/SafeLinkModal.tsx | 48 ++++++++++++++++++++ src/components/ui/ConfirmDialog.tsx | 3 ++ src/global/types.ts | 3 ++ src/modules/actions/ui/misc.ts | 9 ++++ 9 files changed, 112 insertions(+), 10 deletions(-) create mode 100644 src/components/main/SafeLinkModal.async.tsx create mode 100644 src/components/main/SafeLinkModal.tsx diff --git a/src/bundles/extra.ts b/src/bundles/extra.ts index 021195619..fed2f2a1c 100644 --- a/src/bundles/extra.ts +++ b/src/bundles/extra.ts @@ -3,6 +3,7 @@ export { default as MediaViewer } from '../components/mediaViewer/MediaViewer'; export { default as ForwardPicker } from '../components/main/ForwardPicker'; export { default as Errors } from '../components/main/Errors'; export { default as Notifications } from '../components/main/Notifications'; +export { default as SafeLinkModal } from '../components/main/SafeLinkModal'; export { default as CalendarModal } from '../components/common/CalendarModal'; export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal'; diff --git a/src/components/common/SafeLink.tsx b/src/components/common/SafeLink.tsx index 350bde5eb..669e75b17 100644 --- a/src/components/common/SafeLink.tsx +++ b/src/components/common/SafeLink.tsx @@ -14,20 +14,31 @@ type OwnProps = { children?: any; }; -type DispatchProps = Pick; +type DispatchProps = Pick; const SafeLink: FC = ({ url, text, className, children, + toggleSafeLinkModal, openTelegramLink, }) => { + const content = children || text; + const isNotSafe = url !== content; + const handleClick = useCallback((e: React.MouseEvent) => { if ( e.ctrlKey || e.altKey || e.shiftKey || e.metaKey || !url || (!url.match(RE_TME_LINK) && !url.match(RE_TME_INVITE_LINK)) ) { + if (isNotSafe) { + toggleSafeLinkModal({ url }); + + e.preventDefault(); + return false; + } + return true; } @@ -35,7 +46,7 @@ const SafeLink: FC = ({ openTelegramLink({ url }); return false; - }, [openTelegramLink, url]); + }, [isNotSafe, openTelegramLink, toggleSafeLinkModal, url]); if (!url) { return undefined; @@ -48,32 +59,32 @@ const SafeLink: FC = ({ return ( - {children || text} + {content} ); }; -function getHref(url?: string) { +function ensureProtocol(url?: string) { if (!url) { return undefined; } - return url.includes('://') ? url : `http://${url}`; + return url.includes('://') ? url : `https://${url}`; } -function getDecodedUrl(url?: string) { +function getDomain(url?: string) { if (!url) { return undefined; } - const href = getHref(url); + const href = ensureProtocol(url); if (!href) { return undefined; } @@ -101,5 +112,7 @@ function getDecodedUrl(url?: string) { export default memo(withGlobal( undefined, - (setGlobal, actions): DispatchProps => pick(actions, ['openTelegramLink']), + (setGlobal, actions): DispatchProps => pick(actions, [ + 'toggleSafeLinkModal', 'openTelegramLink', + ]), )(SafeLink)); diff --git a/src/components/common/helpers/renderText.tsx b/src/components/common/helpers/renderText.tsx index dfa5104a2..98da3f83f 100644 --- a/src/components/common/helpers/renderText.tsx +++ b/src/components/common/helpers/renderText.tsx @@ -205,6 +205,10 @@ function addLinks(textParts: TextPart[]): TextPart[] { , ); } else { + if (nextLink.endsWith('?')) { + nextLink = nextLink.slice(0, nextLink.length - 1); + } + content.push( , ); diff --git a/src/components/main/Main.tsx b/src/components/main/Main.tsx index 0a8e46ae5..3b5cd205e 100644 --- a/src/components/main/Main.tsx +++ b/src/components/main/Main.tsx @@ -29,6 +29,7 @@ import AudioPlayer from '../middle/AudioPlayer'; import Notifications from './Notifications.async'; import Errors from './Errors.async'; import ForwardPicker from './ForwardPicker.async'; +import SafeLinkModal from './SafeLinkModal.async'; import './Main.scss'; @@ -42,6 +43,7 @@ type StateProps = { hasNotifications: boolean; hasErrors: boolean; audioMessage?: ApiMessage; + safeLinkModalUrl?: string; }; type DispatchProps = Pick; @@ -65,6 +67,7 @@ const Main: FC = ({ hasNotifications, hasErrors, audioMessage, + safeLinkModalUrl, }) => { if (DEBUG && !DEBUG_isLogged) { DEBUG_isLogged = true; @@ -167,6 +170,7 @@ const Main: FC = ({ {audioMessage && } + ); }; @@ -201,6 +205,7 @@ export default memo(withGlobal( hasNotifications: Boolean(global.notifications.length), hasErrors: Boolean(global.errors.length), audioMessage, + safeLinkModalUrl: global.safeLinkModalUrl, }; }, (setGlobal, actions): DispatchProps => pick(actions, ['loadAnimatedEmojis']), diff --git a/src/components/main/SafeLinkModal.async.tsx b/src/components/main/SafeLinkModal.async.tsx new file mode 100644 index 000000000..1010c5c3b --- /dev/null +++ b/src/components/main/SafeLinkModal.async.tsx @@ -0,0 +1,16 @@ +import React, { FC, memo } from '../../lib/teact/teact'; +import { Bundles } from '../../util/moduleLoader'; + +import { OwnProps } from './SafeLinkModal'; + +import useModuleLoader from '../../hooks/useModuleLoader'; + +const SafeLinkModalAsync: FC = (props) => { + const { url } = props; + const SafeLinkModal = useModuleLoader(Bundles.Extra, 'SafeLinkModal', !url); + + // eslint-disable-next-line react/jsx-props-no-spreading + return SafeLinkModal ? : undefined; +}; + +export default memo(SafeLinkModalAsync); diff --git a/src/components/main/SafeLinkModal.tsx b/src/components/main/SafeLinkModal.tsx new file mode 100644 index 000000000..d9b270291 --- /dev/null +++ b/src/components/main/SafeLinkModal.tsx @@ -0,0 +1,48 @@ +import React, { FC, memo, useCallback } from '../../lib/teact/teact'; +import { withGlobal } from '../../lib/teact/teactn'; + +import { GlobalActions } from '../../global/types'; + +import { pick } from '../../util/iteratees'; +import renderText from '../common/helpers/renderText'; +import useLang from '../../hooks/useLang'; +import useCurrentOrPrev from '../../hooks/useCurrentOrPrev'; + +import ConfirmDialog from '../ui/ConfirmDialog'; + +export type OwnProps = { + url?: string; +}; + +type DispatchProps = Pick; + +const SafeLinkModal: FC = ({ url, toggleSafeLinkModal }) => { + const lang = useLang(); + + const handleOpen = useCallback(() => { + window.open(url); + toggleSafeLinkModal({ url: undefined }); + }, [toggleSafeLinkModal, url]); + + const handleDismiss = useCallback(() => { + toggleSafeLinkModal({ url: undefined }); + }, [toggleSafeLinkModal]); + + const renderingUrl = useCurrentOrPrev(url); + + return ( + + ); +}; + +export default memo(withGlobal( + undefined, + (setGlobal, actions): DispatchProps => pick(actions, ['toggleSafeLinkModal']), +)(SafeLinkModal)); diff --git a/src/components/ui/ConfirmDialog.tsx b/src/components/ui/ConfirmDialog.tsx index 024b81308..0303c5b4c 100644 --- a/src/components/ui/ConfirmDialog.tsx +++ b/src/components/ui/ConfirmDialog.tsx @@ -10,6 +10,7 @@ type OwnProps = { isOpen: boolean; onClose: () => void; onCloseAnimationEnd?: () => void; + title?: string; header?: FC; textParts?: TextPart[]; text?: string; @@ -23,6 +24,7 @@ const ConfirmDialog: FC = ({ isOpen, onClose, onCloseAnimationEnd, + title, header, text, textParts, @@ -36,6 +38,7 @@ const ConfirmDialog: FC = ({ return ( { errors: newErrors, }; }); + +addReducer('toggleSafeLinkModal', (global, actions, payload) => { + const { url: safeLinkModalUrl } = payload; + + return { + ...global, + safeLinkModalUrl, + }; +});