Safe Link Modal: Confirm when opening external links
This commit is contained in:
parent
e7e7611af6
commit
e83e611caa
@ -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';
|
||||
|
||||
@ -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));
|
||||
|
||||
@ -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} />,
|
||||
);
|
||||
|
||||
@ -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']),
|
||||
|
||||
16
src/components/main/SafeLinkModal.async.tsx
Normal file
16
src/components/main/SafeLinkModal.async.tsx
Normal 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);
|
||||
48
src/components/main/SafeLinkModal.tsx
Normal file
48
src/components/main/SafeLinkModal.tsx
Normal 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));
|
||||
@ -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}
|
||||
|
||||
@ -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' |
|
||||
|
||||
@ -190,3 +190,12 @@ addReducer('dismissError', (global) => {
|
||||
errors: newErrors,
|
||||
};
|
||||
});
|
||||
|
||||
addReducer('toggleSafeLinkModal', (global, actions, payload) => {
|
||||
const { url: safeLinkModalUrl } = payload;
|
||||
|
||||
return {
|
||||
...global,
|
||||
safeLinkModalUrl,
|
||||
};
|
||||
});
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user