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 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';
|
||||||
|
|||||||
@ -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));
|
||||||
|
|||||||
@ -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} />,
|
||||||
);
|
);
|
||||||
|
|||||||
@ -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']),
|
||||||
|
|||||||
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;
|
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}
|
||||||
|
|||||||
@ -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' |
|
||||||
|
|||||||
@ -190,3 +190,12 @@ addReducer('dismissError', (global) => {
|
|||||||
errors: newErrors,
|
errors: newErrors,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
|
addReducer('toggleSafeLinkModal', (global, actions, payload) => {
|
||||||
|
const { url: safeLinkModalUrl } = payload;
|
||||||
|
|
||||||
|
return {
|
||||||
|
...global,
|
||||||
|
safeLinkModalUrl,
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user