Merge cbb309e422
into 2d871ef0d9
commit
776a565007
@ -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>
|
||||
);
|
||||
};
|
@ -0,0 +1,168 @@
|
||||
import { shell } from 'electron';
|
||||
import React, { Dispatch } from 'react';
|
||||
import useKey from 'react-use/lib/useKey';
|
||||
import styled from 'styled-components';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { MessageInteraction } from '../../interactions';
|
||||
import { SessionWrapperModal } from '../SessionWrapperModal';
|
||||
import { SessionButton, SessionButtonColor, SessionButtonType } from '../basic/SessionButton';
|
||||
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 }>`
|
||||
font-size: var(--font-size-md);
|
||||
line-height: 1.5;
|
||||
margin-bottom: var(--margins-lg);
|
||||
|
||||
max-width: ${props =>
|
||||
props.textLength > 90
|
||||
? '60ch'
|
||||
: '33ch'}; // this is ugly, but we want the dialog description to have multiple lines when a short text is displayed
|
||||
`;
|
||||
|
||||
const StyledExternalLinkContainer = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
border: 1px solid var(--input-border-color);
|
||||
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;
|
||||
}
|
||||
|
||||
export const SessionOpenExternalLinkDialog = ({
|
||||
urlToOpen,
|
||||
}: SessionOpenExternalLinkDialogProps) => {
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useKey('Enter', () => {
|
||||
handleOpen();
|
||||
});
|
||||
|
||||
useKey('Escape', () => {
|
||||
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));
|
||||
};
|
||||
|
||||
const handleTrust = () => {
|
||||
void TrustedWebsitesController.addToTrusted(hostname!);
|
||||
handleOpen();
|
||||
};
|
||||
|
||||
return (
|
||||
<SessionWrapperModal
|
||||
title={window.i18n('linkVisitWarningTitle')}
|
||||
onClose={() => 0}
|
||||
showExitIcon={false}
|
||||
showHeader
|
||||
>
|
||||
<SpacerLG />
|
||||
|
||||
<div className="session-modal__centered">
|
||||
<StyledSubText tag="span" textLength={message.length} html={message} />
|
||||
<StyledExternalLinkContainer>
|
||||
<StyledExternalLinkInput readOnly value={urlToOpen} />
|
||||
<SessionIconButton
|
||||
aria-label={window.i18n('editMenuCopy')}
|
||||
iconType="copy"
|
||||
iconSize="small"
|
||||
onClick={handleCopy}
|
||||
/>
|
||||
</StyledExternalLinkContainer>
|
||||
</div>
|
||||
|
||||
<SpacerLG />
|
||||
|
||||
<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 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,
|
||||
})
|
||||
);
|
||||
}
|
||||
};
|
@ -0,0 +1,167 @@
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import useUpdate from 'react-use/lib/useUpdate';
|
||||
import styled from 'styled-components';
|
||||
import { useSet } from '../../hooks/useSet';
|
||||
import { ToastUtils } from '../../session/utils';
|
||||
import { TrustedWebsitesController } from '../../util';
|
||||
import { SessionButton, SessionButtonColor } from '../basic/SessionButton';
|
||||
import { SpacerLG } from '../basic/Text';
|
||||
import { SessionIconButton } from '../icon';
|
||||
import { SettingsTitleAndDescription } from './SessionSettingListItem';
|
||||
import { TrustedWebsiteListItem } from '../TrustedWebsiteListItem';
|
||||
|
||||
const TrustedEntriesContainer = styled.div`
|
||||
flex-shrink: 1;
|
||||
overflow: auto;
|
||||
min-height: 40px;
|
||||
max-height: 100%;
|
||||
`;
|
||||
|
||||
const TrustedEntriesRoundedContainer = styled.div`
|
||||
overflow: hidden;
|
||||
background: var(--background-secondary-color);
|
||||
border: 1px solid var(--border-color);
|
||||
border-radius: 16px;
|
||||
padding: var(--margins-lg);
|
||||
margin: 0 var(--margins-lg);
|
||||
`;
|
||||
|
||||
const TrustedWebsitesSection = styled.div`
|
||||
flex-shrink: 0;
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
min-height: 80px;
|
||||
|
||||
background: var(--settings-tab-background-color);
|
||||
color: var(--settings-tab-text-color);
|
||||
border-top: 1px solid var(--border-color);
|
||||
border-bottom: 1px solid var(--border-color);
|
||||
|
||||
margin-bottom: var(--margins-lg);
|
||||
`;
|
||||
|
||||
const TrustedWebsitesListTitle = styled.div`
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
min-height: 45px;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
const TrustedWebsitesListTitleButtons = styled.div`
|
||||
display: flex;
|
||||
align-items: center;
|
||||
`;
|
||||
|
||||
export const StyledTrustedSettingItem = styled.div<{ clickable: boolean }>`
|
||||
font-size: var(--font-size-md);
|
||||
padding: var(--margins-lg);
|
||||
|
||||
cursor: ${props => (props.clickable ? 'pointer' : 'unset')};
|
||||
`;
|
||||
|
||||
const TrustedEntries = (props: {
|
||||
trustedHostnames: Array<string>;
|
||||
selectedHostnames: Array<string>;
|
||||
addToSelected: (id: string) => void;
|
||||
removeFromSelected: (id: string) => void;
|
||||
}) => {
|
||||
const { addToSelected, trustedHostnames, removeFromSelected, selectedHostnames } = props;
|
||||
return (
|
||||
<TrustedEntriesRoundedContainer>
|
||||
<TrustedEntriesContainer>
|
||||
{trustedHostnames.map(trustedEntry => {
|
||||
return (
|
||||
<TrustedWebsiteListItem
|
||||
hostname={trustedEntry}
|
||||
isSelected={selectedHostnames.includes(trustedEntry)}
|
||||
key={trustedEntry}
|
||||
onSelect={addToSelected}
|
||||
onUnselect={removeFromSelected}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</TrustedEntriesContainer>
|
||||
</TrustedEntriesRoundedContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export const TrustedWebsitesList = () => {
|
||||
const [expanded, setExpanded] = useState(false);
|
||||
const {
|
||||
uniqueValues: selectedHostnames,
|
||||
addTo: addToSelected,
|
||||
removeFrom: removeFromSelected,
|
||||
empty: emptySelected,
|
||||
} = useSet<string>([]);
|
||||
|
||||
const forceUpdate = useUpdate();
|
||||
|
||||
const hasAtLeastOneSelected = Boolean(selectedHostnames.length);
|
||||
const trustedWebsites = TrustedWebsitesController.getTrustedWebsites();
|
||||
const noTrustedWebsites = !trustedWebsites.length;
|
||||
|
||||
function toggleTrustedWebsitesList() {
|
||||
if (trustedWebsites.length) {
|
||||
setExpanded(!expanded);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeTrustedWebsites() {
|
||||
if (selectedHostnames.length) {
|
||||
await TrustedWebsitesController.removeFromTrusted(selectedHostnames);
|
||||
emptySelected();
|
||||
ToastUtils.pushToastSuccess('removed', window.i18n('removed'));
|
||||
forceUpdate();
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<TrustedWebsitesSection>
|
||||
<StyledTrustedSettingItem clickable={!noTrustedWebsites}>
|
||||
<TrustedWebsitesListTitle onClick={toggleTrustedWebsitesList}>
|
||||
<SettingsTitleAndDescription
|
||||
title={window.i18n('trustedWebsites')}
|
||||
description={window.i18n('trustedWebsitesDescription')}
|
||||
/>
|
||||
{noTrustedWebsites ? (
|
||||
<NoTrustedWebsites />
|
||||
) : (
|
||||
<TrustedWebsitesListTitleButtons>
|
||||
{hasAtLeastOneSelected && expanded ? (
|
||||
<SessionButton
|
||||
buttonColor={SessionButtonColor.Danger}
|
||||
text={window.i18n('remove')}
|
||||
onClick={removeTrustedWebsites}
|
||||
/>
|
||||
) : null}
|
||||
<SpacerLG />
|
||||
<SessionIconButton
|
||||
iconSize={'large'}
|
||||
iconType={'chevron'}
|
||||
onClick={toggleTrustedWebsitesList}
|
||||
iconRotation={expanded ? 180 : 0}
|
||||
/>
|
||||
</TrustedWebsitesListTitleButtons>
|
||||
)}
|
||||
</TrustedWebsitesListTitle>
|
||||
</StyledTrustedSettingItem>
|
||||
{expanded && !noTrustedWebsites ? (
|
||||
<>
|
||||
<TrustedEntries
|
||||
trustedHostnames={trustedWebsites}
|
||||
selectedHostnames={selectedHostnames}
|
||||
addToSelected={addToSelected}
|
||||
removeFromSelected={removeFromSelected}
|
||||
/>
|
||||
<SpacerLG />
|
||||
</>
|
||||
) : null}
|
||||
</TrustedWebsitesSection>
|
||||
);
|
||||
};
|
||||
|
||||
const NoTrustedWebsites = () => {
|
||||
return <div>{window.i18n('noTrustedWebsitesEntries')}</div>;
|
||||
};
|
@ -0,0 +1,67 @@
|
||||
import { Data } from '../data/data';
|
||||
import { Storage } from './storage';
|
||||
|
||||
const TRUSTED_WEBSITES_ID = 'trusted-websites';
|
||||
|
||||
export class TrustedWebsitesController {
|
||||
private static loaded: boolean = false;
|
||||
private static trustedWebsites: Set<string> = new Set();
|
||||
|
||||
public static isTrusted(hostname: string): boolean {
|
||||
return this.trustedWebsites.has(hostname);
|
||||
}
|
||||
|
||||
public static async addToTrusted(hostname: string): Promise<void> {
|
||||
await this.load();
|
||||
if (!this.trustedWebsites.has(hostname)) {
|
||||
this.trustedWebsites.add(hostname);
|
||||
await this.saveToDB(TRUSTED_WEBSITES_ID, this.trustedWebsites);
|
||||
}
|
||||
}
|
||||
|
||||
public static async removeFromTrusted(hostnames: Array<string>): Promise<void> {
|
||||
await this.load();
|
||||
let changes = false;
|
||||
hostnames.forEach(hostname => {
|
||||
if (this.trustedWebsites.has(hostname)) {
|
||||
this.trustedWebsites.delete(hostname);
|
||||
changes = true;
|
||||
}
|
||||
});
|
||||
|
||||
if (changes) {
|
||||
await this.saveToDB(TRUSTED_WEBSITES_ID, this.trustedWebsites);
|
||||
}
|
||||
}
|
||||
|
||||
public static getTrustedWebsites(): Array<string> {
|
||||
return [...this.trustedWebsites];
|
||||
}
|
||||
|
||||
// ---- DB
|
||||
|
||||
public static async load() {
|
||||
if (!this.loaded) {
|
||||
this.trustedWebsites = await this.getTrustedWebsitesFromDB(TRUSTED_WEBSITES_ID);
|
||||
this.loaded = true;
|
||||
}
|
||||
}
|
||||
|
||||
public static reset() {
|
||||
this.loaded = false;
|
||||
this.trustedWebsites = new Set();
|
||||
}
|
||||
|
||||
private static async getTrustedWebsitesFromDB(id: string): Promise<Set<string>> {
|
||||
const data = await Data.getItemById(id);
|
||||
if (!data || !data.value) {
|
||||
return new Set();
|
||||
}
|
||||
|
||||
return new Set(data.value);
|
||||
}
|
||||
|
||||
private static async saveToDB(id: string, hostnames: Set<string>): Promise<void> {
|
||||
await Storage.put(id, [...hostnames]);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue