Safe Link Modal: Confirm when opening external links

This commit is contained in:
Alexander Zinchuk 2021-05-14 13:52:41 +03:00
parent e7e7611af6
commit e83e611caa
9 changed files with 112 additions and 10 deletions

View File

@ -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';

View File

@ -14,20 +14,31 @@ type OwnProps = {
children?: any;
};
type DispatchProps = Pick<GlobalActions, 'openTelegramLink'>;
type DispatchProps = Pick<GlobalActions, 'toggleSafeLinkModal' | 'openTelegramLink'>;
const SafeLink: FC<OwnProps & DispatchProps> = ({
url,
text,
className,
children,
toggleSafeLinkModal,
openTelegramLink,
}) => {
const content = children || text;
const isNotSafe = url !== content;
const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, 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<OwnProps & DispatchProps> = ({
openTelegramLink({ url });
return false;
}, [openTelegramLink, url]);
}, [isNotSafe, openTelegramLink, toggleSafeLinkModal, url]);
if (!url) {
return undefined;
@ -48,32 +59,32 @@ const SafeLink: FC<OwnProps & DispatchProps> = ({
return (
<a
href={getHref(url)}
title={getDecodedUrl(url)}
href={ensureProtocol(url)}
title={getDomain(url)}
target="_blank"
rel="noopener noreferrer"
className={classNames}
onClick={handleClick}
>
{children || text}
{content}
</a>
);
};
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<OwnProps>(
undefined,
(setGlobal, actions): DispatchProps => pick(actions, ['openTelegramLink']),
(setGlobal, actions): DispatchProps => pick(actions, [
'toggleSafeLinkModal', 'openTelegramLink',
]),
)(SafeLink));

View File

@ -205,6 +205,10 @@ function addLinks(textParts: TextPart[]): TextPart[] {
</MentionLink>,
);
} else {
if (nextLink.endsWith('?')) {
nextLink = nextLink.slice(0, nextLink.length - 1);
}
content.push(
<SafeLink text={nextLink} url={nextLink} />,
);

View File

@ -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<GlobalActions, 'loadAnimatedEmojis'>;
@ -65,6 +67,7 @@ const Main: FC<StateProps & DispatchProps> = ({
hasNotifications,
hasErrors,
audioMessage,
safeLinkModalUrl,
}) => {
if (DEBUG && !DEBUG_isLogged) {
DEBUG_isLogged = true;
@ -167,6 +170,7 @@ const Main: FC<StateProps & DispatchProps> = ({
<Notifications isOpen={hasNotifications} />
<Errors isOpen={hasErrors} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
<SafeLinkModal url={safeLinkModalUrl} />
</div>
);
};
@ -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']),

View File

@ -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<OwnProps> = (props) => {
const { url } = props;
const SafeLinkModal = useModuleLoader(Bundles.Extra, 'SafeLinkModal', !url);
// eslint-disable-next-line react/jsx-props-no-spreading
return SafeLinkModal ? <SafeLinkModal {...props} /> : undefined;
};
export default memo(SafeLinkModalAsync);

View File

@ -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<GlobalActions, 'toggleSafeLinkModal'>;
const SafeLinkModal: FC<OwnProps & DispatchProps> = ({ 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 (
<ConfirmDialog
isOpen={Boolean(url)}
onClose={handleDismiss}
title={lang('OpenUrlTitle')}
textParts={renderText(lang('OpenUrlAlert2', renderingUrl), ['links'])}
confirmLabel={lang('OpenUrlTitle')}
confirmHandler={handleOpen}
/>
);
};
export default memo(withGlobal<OwnProps>(
undefined,
(setGlobal, actions): DispatchProps => pick(actions, ['toggleSafeLinkModal']),
)(SafeLinkModal));

View File

@ -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<OwnProps> = ({
isOpen,
onClose,
onCloseAnimationEnd,
title,
header,
text,
textParts,
@ -36,6 +38,7 @@ const ConfirmDialog: FC<OwnProps> = ({
return (
<Modal
className="confirm"
title={title}
header={header}
isOpen={isOpen}
onClose={onClose}

View File

@ -379,6 +379,8 @@ export type GlobalState = {
deviceToken: string;
subscribedAt: number;
};
safeLinkModalUrl?: string;
};
export type ActionTypes = (
@ -387,6 +389,7 @@ export type ActionTypes = (
'showNotification' | 'dismissNotification' | 'showError' | 'dismissError' |
// ui
'toggleChatInfo' | 'setIsUiReady' | 'addRecentEmoji' | 'addRecentSticker' | 'toggleLeftColumn' |
'toggleSafeLinkModal' |
// auth
'setAuthPhoneNumber' | 'setAuthCode' | 'setAuthPassword' | 'signUp' | 'returnToAuthPhoneNumber' | 'signOut' |
'setAuthRememberMe' | 'clearAuthError' | 'uploadProfilePhoto' | 'gotToAuthQrCode' | 'clearCache' |

View File

@ -190,3 +190,12 @@ addReducer('dismissError', (global) => {
errors: newErrors,
};
});
addReducer('toggleSafeLinkModal', (global, actions, payload) => {
const { url: safeLinkModalUrl } = payload;
return {
...global,
safeLinkModalUrl,
};
});