Skip to content

Commit

Permalink
feat: trusted websites list and trust hostname btn
Browse files Browse the repository at this point in the history
This commit adds the trusted websites list via TrustedWebsitesController
and "Trust [hostname]" button to OpenExternalLinkDialog.

Need help with resolving issue with persisting it to db.

See #2662.
  • Loading branch information
VityaSchel committed Apr 27, 2024
1 parent cd1ba11 commit cbb309e
Show file tree
Hide file tree
Showing 13 changed files with 421 additions and 36 deletions.
7 changes: 6 additions & 1 deletion _locales/en/messages.json
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,7 @@
"block": "Block",
"unblock": "Unblock",
"unblocked": "Unblocked",
"removed": "Removed",
"blocked": "Blocked",
"blockedSettingsTitle": "Blocked Contacts",
"conversationsSettingsTitle": "Conversations",
Expand Down Expand Up @@ -579,5 +580,9 @@
"duration": "Duration",
"notApplicable": "N/A",
"unknownError": "Unknown Error",
"displayNameErrorNew": "We were unable to load your display name. Please enter a new display name to continue."
"displayNameErrorNew": "We were unable to load your display name. Please enter a new display name to continue.",
"trustHostname": "Trust $hostname$",
"trustedWebsites": "Trusted websites",
"trustedWebsitesDescription": "Clicking on a trusted website will open it in your browser.",
"noTrustedWebsitesEntries": "You have no trusted websites."
}
82 changes: 82 additions & 0 deletions ts/components/TrustedWebsiteListItem.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import React from 'react';
import styled from 'styled-components';

import { SessionRadio } from './basic/SessionRadio';

const StyledTrustedWebsiteItem = styled.button<{
inMentions?: boolean;
zombie?: boolean;
selected?: boolean;
disableBg?: boolean;
}>`
cursor: pointer;
display: flex;
align-items: center;
justify-content: space-between;
flex-shrink: 0;
flex-grow: 1;
font-family: var(--font-default);
padding: 0px var(--margins-sm);
height: ${props => (props.inMentions ? '40px' : '50px')};
width: 100%;
transition: var(--default-duration);
opacity: ${props => (props.zombie ? 0.5 : 1)};
background-color: ${props =>
!props.disableBg && props.selected
? 'var(--conversation-tab-background-selected-color) !important'
: null};
:not(:last-child) {
border-bottom: 1px solid var(--border-color);
}
`;

const StyledInfo = styled.div`
display: flex;
align-items: center;
min-width: 0;
`;

const StyledName = styled.span`
font-weight: bold;
margin-inline-start: var(--margins-md);
margin-inline-end: var(--margins-md);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
`;

const StyledCheckContainer = styled.div`
display: flex;
align-items: center;
`;

export const TrustedWebsiteListItem = (props: {
hostname: string;
isSelected: boolean;
onSelect?: (pubkey: string) => void;
onUnselect?: (pubkey: string) => void;
}) => {
const { hostname, isSelected, onSelect, onUnselect } = props;

return (
<StyledTrustedWebsiteItem
onClick={() => {
if (isSelected) {
onUnselect?.(hostname);
} else {
onSelect?.(hostname);
}
}}
selected={isSelected}
>
<StyledInfo>
<StyledName>{hostname}</StyledName>
</StyledInfo>

<StyledCheckContainer>
<SessionRadio active={isSelected} value={hostname} inputName={hostname} label="" />
</StyledCheckContainer>
</StyledTrustedWebsiteItem>
);
};
1 change: 1 addition & 0 deletions ts/components/basic/SessionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ export enum SessionButtonColor {
Orange = 'orange',
Red = 'red',
White = 'white',
Grey = 'grey',
Primary = 'primary',
Danger = 'danger',
None = 'transparent',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import styled from 'styled-components';
import { RenderTextCallbackType } from '../../../../types/Util';
import { getEmojiSizeClass, SizeClassType } from '../../../../util/emoji';
import { LinkPreviews } from '../../../../util/linkPreviews';
import { showLinkVisitWarningDialog } from '../../../dialog/OpenExternalLinkDialog';
import { promptToOpenExternalLink } from '../../../dialog/OpenExternalLinkDialog';
import { AddMentions } from '../../AddMentions';
import { AddNewLines } from '../../AddNewLines';
import { Emojify } from '../../Emojify';
Expand Down Expand Up @@ -128,7 +128,7 @@ const Linkify = (props: LinkifyProps): JSX.Element => {
onClick={e => {
e.preventDefault();
e.stopPropagation();
showLinkVisitWarningDialog(url, dispatch);
promptToOpenExternalLink(url, dispatch);
}}
>
{originalText}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import {
} from '../../../../state/selectors';
import { useIsMessageSelectionMode } from '../../../../state/selectors/selectedConversation';
import { isImageAttachment } from '../../../../types/Attachment';
import { showLinkVisitWarningDialog } from '../../../dialog/OpenExternalLinkDialog';
import { promptToOpenExternalLink } from '../../../dialog/OpenExternalLinkDialog';
import { SessionIcon } from '../../../icon';
import { Image } from '../../Image';

Expand Down Expand Up @@ -57,7 +57,7 @@ export const MessageLinkPreview = (props: Props) => {
return;
}
if (previews?.length && previews[0].url) {
showLinkVisitWarningDialog(previews[0].url, dispatch);
promptToOpenExternalLink(previews[0].url, dispatch);
}
}

Expand Down
4 changes: 3 additions & 1 deletion ts/components/dialog/ModalContainer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,9 @@ export const ModalContainer = () => {
{sessionPasswordModalState && <SessionPasswordDialog {...sessionPasswordModalState} />}
{deleteAccountModalState && <DeleteAccountModal {...deleteAccountModalState} />}
{confirmModalState && <SessionConfirm {...confirmModalState} />}
{openExternalLinkModalState && <SessionOpenExternalLinkDialog {...openExternalLinkModalState} />}
{openExternalLinkModalState && (
<SessionOpenExternalLinkDialog {...openExternalLinkModalState} />
)}
{reactListModalState && <ReactListModal {...reactListModalState} />}
{reactClearAllModalState && <ReactClearAllModal {...reactClearAllModalState} />}
{editProfilePictureModalState && (
Expand Down
108 changes: 79 additions & 29 deletions ts/components/dialog/OpenExternalLinkDialog.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,9 @@ import { SessionHtmlRenderer } from '../basic/SessionHTMLRenderer';
import { SpacerLG } from '../basic/Text';
import { setOpenExternalLinkModal } from '../../state/ducks/modalDialog';
import { SessionIconButton } from '../icon';
import { TrustedWebsitesController } from '../../util';

const StyledSubText = styled(SessionHtmlRenderer) <{ textLength: number }>`
const StyledSubText = styled(SessionHtmlRenderer)<{ textLength: number }>`
font-size: var(--font-size-md);
line-height: 1.5;
margin-bottom: var(--margins-lg);
Expand All @@ -29,19 +30,30 @@ const StyledExternalLinkContainer = styled.div`
border-radius: 6px;
transition: var(--default-duration);
width: 100%;
`
`;

const StyledExternalLinkInput = styled.input`
font: inherit;
border: none !important;
flex: 1;
`
`;

const StyledActionButtons = styled.div`
display: flex;
flex-direction: column;
& > button {
font-weight: 400;
}
`;

interface SessionOpenExternalLinkDialogProps {
urlToOpen: string
urlToOpen: string;
}

export const SessionOpenExternalLinkDialog = ({ urlToOpen }: SessionOpenExternalLinkDialogProps) => {
export const SessionOpenExternalLinkDialog = ({
urlToOpen,
}: SessionOpenExternalLinkDialogProps) => {
const dispatch = useDispatch();

useKey('Enter', () => {
Expand All @@ -52,21 +64,36 @@ export const SessionOpenExternalLinkDialog = ({ urlToOpen }: SessionOpenExternal
handleClose();
});

// TODO: replace translations to remove $url$ dynamic varialbe,
// instead put this variable below in the readonly input
const message = window.i18n('linkVisitWarningMessage', ['URL']);

const hostname: string | null = React.useMemo(() => {
try {
const url = new URL(urlToOpen);
return url.hostname;
} catch (e) {
return null;
}
}, [urlToOpen]);

const handleOpen = () => {
void shell.openExternal(urlToOpen);
handleClose();
};

const handleCopy = () => {
MessageInteraction.copyBodyToClipboard(urlToOpen);
};

const handleClose = () => {
dispatch(setOpenExternalLinkModal(null))
}
dispatch(setOpenExternalLinkModal(null));
};

// TODO: replace translations to remove $url$ dynamic varialbe,
// instead put this variable below in the readonly input
const message = window.i18n('linkVisitWarningMessage', ['URL']);
const handleTrust = () => {
void TrustedWebsitesController.addToTrusted(hostname!);
handleOpen();
};

return (
<SessionWrapperModal
Expand All @@ -92,27 +119,50 @@ export const SessionOpenExternalLinkDialog = ({ urlToOpen }: SessionOpenExternal

<SpacerLG />

<div className="session-modal__button-group">
<SessionButton
text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple}
onClick={handleClose}
/>
<SessionButton
text={window.i18n('open')}
buttonColor={SessionButtonColor.Primary}
buttonType={SessionButtonType.Simple}
onClick={handleOpen}
/>
</div>
<StyledActionButtons>
<div className="session-modal__button-group">
<SessionButton
text={window.i18n('cancel')}
buttonType={SessionButtonType.Simple}
onClick={handleClose}
/>
<SessionButton
text={window.i18n('open')}
buttonColor={SessionButtonColor.Primary}
buttonType={SessionButtonType.Simple}
onClick={handleOpen}
/>
</div>
{hostname && (
<SessionButton
text={window.i18n('trustHostname', [hostname])}
buttonColor={SessionButtonColor.Grey}
buttonType={SessionButtonType.Simple}
onClick={handleTrust}
/>
)}
</StyledActionButtons>
</SessionWrapperModal>
);
};

export const showLinkVisitWarningDialog = (urlToOpen: string, dispatch: Dispatch<any>) => {
dispatch(
setOpenExternalLinkModal({
urlToOpen,
})
);
export const promptToOpenExternalLink = (urlToOpen: string, dispatch: Dispatch<any>) => {
let hostname: string | null;

try {
const url = new URL(urlToOpen);
hostname = url.hostname;
} catch (e) {
hostname = null;
}

if (hostname && TrustedWebsitesController.isTrusted(hostname)) {
void shell.openExternal(urlToOpen);
} else {
dispatch(
setOpenExternalLinkModal({
urlToOpen,
})
);
}
};
4 changes: 3 additions & 1 deletion ts/components/settings/BlockedList.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ const BlockedEntriesRoundedContainer = styled.div`
`;

const BlockedContactsSection = styled.div`
flex-shrink: 0;
display: flex;
flex-direction: column;
min-height: 80px;
Expand Down Expand Up @@ -142,7 +144,7 @@ export const BlockedContactsList = () => {
iconSize={'large'}
iconType={'chevron'}
onClick={toggleUnblockList}
iconRotation={expanded ? 0 : 180}
iconRotation={expanded ? 180 : 0}
dataTestId="reveal-blocked-user-settings"
/>
</BlockedContactListTitleButtons>
Expand Down
Loading

0 comments on commit cbb309e

Please sign in to comment.