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 ForwardPicker } from '../components/main/ForwardPicker';
export { default as Errors } from '../components/main/Errors'; export { default as Errors } from '../components/main/Errors';
export { default as Notifications } from '../components/main/Notifications'; 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 CalendarModal } from '../components/common/CalendarModal';
export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal'; export { default as DeleteMessageModal } from '../components/common/DeleteMessageModal';

View File

@ -14,20 +14,31 @@ type OwnProps = {
children?: any; children?: any;
}; };
type DispatchProps = Pick<GlobalActions, 'openTelegramLink'>; type DispatchProps = Pick<GlobalActions, 'toggleSafeLinkModal' | 'openTelegramLink'>;
const SafeLink: FC<OwnProps & DispatchProps> = ({ const SafeLink: FC<OwnProps & DispatchProps> = ({
url, url,
text, text,
className, className,
children, children,
toggleSafeLinkModal,
openTelegramLink, openTelegramLink,
}) => { }) => {
const content = children || text;
const isNotSafe = url !== content;
const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => { const handleClick = useCallback((e: React.MouseEvent<HTMLAnchorElement, MouseEvent>) => {
if ( if (
e.ctrlKey || e.altKey || e.shiftKey || e.metaKey e.ctrlKey || e.altKey || e.shiftKey || e.metaKey
|| !url || (!url.match(RE_TME_LINK) && !url.match(RE_TME_INVITE_LINK)) || !url || (!url.match(RE_TME_LINK) && !url.match(RE_TME_INVITE_LINK))
) { ) {
if (isNotSafe) {
toggleSafeLinkModal({ url });
e.preventDefault();
return false;
}
return true; return true;
} }
@ -35,7 +46,7 @@ const SafeLink: FC<OwnProps & DispatchProps> = ({
openTelegramLink({ url }); openTelegramLink({ url });
return false; return false;
}, [openTelegramLink, url]); }, [isNotSafe, openTelegramLink, toggleSafeLinkModal, url]);
if (!url) { if (!url) {
return undefined; return undefined;
@ -48,32 +59,32 @@ const SafeLink: FC<OwnProps & DispatchProps> = ({
return ( return (
<a <a
href={getHref(url)} href={ensureProtocol(url)}
title={getDecodedUrl(url)} title={getDomain(url)}
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className={classNames} className={classNames}
onClick={handleClick} onClick={handleClick}
> >
{children || text} {content}
</a> </a>
); );
}; };
function getHref(url?: string) { function ensureProtocol(url?: string) {
if (!url) { if (!url) {
return undefined; return undefined;
} }
return url.includes('://') ? url : `http://${url}`; return url.includes('://') ? url : `https://${url}`;
} }
function getDecodedUrl(url?: string) { function getDomain(url?: string) {
if (!url) { if (!url) {
return undefined; return undefined;
} }
const href = getHref(url); const href = ensureProtocol(url);
if (!href) { if (!href) {
return undefined; return undefined;
} }
@ -101,5 +112,7 @@ function getDecodedUrl(url?: string) {
export default memo(withGlobal<OwnProps>( export default memo(withGlobal<OwnProps>(
undefined, undefined,
(setGlobal, actions): DispatchProps => pick(actions, ['openTelegramLink']), (setGlobal, actions): DispatchProps => pick(actions, [
'toggleSafeLinkModal', 'openTelegramLink',
]),
)(SafeLink)); )(SafeLink));

View File

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

View File

@ -29,6 +29,7 @@ import AudioPlayer from '../middle/AudioPlayer';
import Notifications from './Notifications.async'; import Notifications from './Notifications.async';
import Errors from './Errors.async'; import Errors from './Errors.async';
import ForwardPicker from './ForwardPicker.async'; import ForwardPicker from './ForwardPicker.async';
import SafeLinkModal from './SafeLinkModal.async';
import './Main.scss'; import './Main.scss';
@ -42,6 +43,7 @@ type StateProps = {
hasNotifications: boolean; hasNotifications: boolean;
hasErrors: boolean; hasErrors: boolean;
audioMessage?: ApiMessage; audioMessage?: ApiMessage;
safeLinkModalUrl?: string;
}; };
type DispatchProps = Pick<GlobalActions, 'loadAnimatedEmojis'>; type DispatchProps = Pick<GlobalActions, 'loadAnimatedEmojis'>;
@ -65,6 +67,7 @@ const Main: FC<StateProps & DispatchProps> = ({
hasNotifications, hasNotifications,
hasErrors, hasErrors,
audioMessage, audioMessage,
safeLinkModalUrl,
}) => { }) => {
if (DEBUG && !DEBUG_isLogged) { if (DEBUG && !DEBUG_isLogged) {
DEBUG_isLogged = true; DEBUG_isLogged = true;
@ -167,6 +170,7 @@ const Main: FC<StateProps & DispatchProps> = ({
<Notifications isOpen={hasNotifications} /> <Notifications isOpen={hasNotifications} />
<Errors isOpen={hasErrors} /> <Errors isOpen={hasErrors} />
{audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />} {audioMessage && <AudioPlayer key={audioMessage.id} message={audioMessage} noUi />}
<SafeLinkModal url={safeLinkModalUrl} />
</div> </div>
); );
}; };
@ -201,6 +205,7 @@ export default memo(withGlobal(
hasNotifications: Boolean(global.notifications.length), hasNotifications: Boolean(global.notifications.length),
hasErrors: Boolean(global.errors.length), hasErrors: Boolean(global.errors.length),
audioMessage, audioMessage,
safeLinkModalUrl: global.safeLinkModalUrl,
}; };
}, },
(setGlobal, actions): DispatchProps => pick(actions, ['loadAnimatedEmojis']), (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; isOpen: boolean;
onClose: () => void; onClose: () => void;
onCloseAnimationEnd?: () => void; onCloseAnimationEnd?: () => void;
title?: string;
header?: FC; header?: FC;
textParts?: TextPart[]; textParts?: TextPart[];
text?: string; text?: string;
@ -23,6 +24,7 @@ const ConfirmDialog: FC<OwnProps> = ({
isOpen, isOpen,
onClose, onClose,
onCloseAnimationEnd, onCloseAnimationEnd,
title,
header, header,
text, text,
textParts, textParts,
@ -36,6 +38,7 @@ const ConfirmDialog: FC<OwnProps> = ({
return ( return (
<Modal <Modal
className="confirm" className="confirm"
title={title}
header={header} header={header}
isOpen={isOpen} isOpen={isOpen}
onClose={onClose} onClose={onClose}

View File

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

View File

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