mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-09 15:18:09 -05:00
feat: Display Plugin Information (#62)
* feat: added info button and confirmation to remove button * refactor: working on plugin info modal * refactor: adding remove and plugininfo to callback dependencies * refactor: working on plugin info modal * feat: show plugin information on plugin upload * refactor: Plugin info and plugin installation modals restyled * refactor: linting * refactor: plugin installation modal refactor * refactor: plugin upload refactor * wip: restyling and add user friendly text * fix: restyling and retexting * refactor PluginInfo * fix error message and twitter profile plugin --------- Co-authored-by: John Shaw <codetrauma@Johns-MacBook-Pro.local> Co-authored-by: tsukino <0xtsukino@gmail.com>
This commit is contained in:
BIN
plugins/reddit/index.wasm
Normal file
BIN
plugins/reddit/index.wasm
Normal file
Binary file not shown.
Binary file not shown.
@@ -37,13 +37,19 @@ export default function Modal(props: Props): ReactElement {
|
||||
}
|
||||
|
||||
type HeaderProps = {
|
||||
className?: string;
|
||||
onClose?: () => void;
|
||||
children: ReactNode;
|
||||
children?: ReactNode;
|
||||
};
|
||||
|
||||
export function ModalHeader(props: HeaderProps): ReactElement {
|
||||
return (
|
||||
<div className={classNames('border-b modal__header border-gray-100')}>
|
||||
<div
|
||||
className={classNames(
|
||||
'border-b modal__header border-gray-100',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
<div className="modal__header__title">{props.children}</div>
|
||||
<div className="modal__header__content">
|
||||
{props.onClose && (
|
||||
|
||||
20
src/components/PluginInfo/index.scss
Normal file
20
src/components/PluginInfo/index.scss
Normal file
@@ -0,0 +1,20 @@
|
||||
.custom-modal {
|
||||
height: 100%;
|
||||
max-width: 800px;
|
||||
max-height: 100vh;
|
||||
display: flex;
|
||||
margin: 0 auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
flex-grow: 2;
|
||||
overflow-y: auto;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.modal__overlay {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
207
src/components/PluginInfo/index.tsx
Normal file
207
src/components/PluginInfo/index.tsx
Normal file
@@ -0,0 +1,207 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
Children,
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import { makePlugin, getPluginConfig } from '../../utils/misc';
|
||||
import { addPlugin } from '../../utils/rpc';
|
||||
import Modal, {
|
||||
ModalHeader,
|
||||
ModalContent,
|
||||
ModalFooter,
|
||||
} from '../../components/Modal/Modal';
|
||||
import type { PluginConfig } from '../../utils/misc';
|
||||
import './index.scss';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
import {
|
||||
HostFunctionsDescriptions,
|
||||
MultipleParts,
|
||||
PermissionDescription,
|
||||
} from '../../utils/plugins';
|
||||
import { ErrorModal } from '../ErrorModal';
|
||||
import classNames from 'classnames';
|
||||
|
||||
export default function PluginUploadInfo(): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [pluginBuffer, setPluginBuffer] = useState<ArrayBuffer | any>(null);
|
||||
const [pluginContent, setPluginContent] = useState<PluginConfig | null>(null);
|
||||
|
||||
const onAddPlugin = useCallback(
|
||||
async (evt: React.MouseEvent<HTMLButtonElement>) => {
|
||||
try {
|
||||
await addPlugin(Buffer.from(pluginBuffer).toString('hex'));
|
||||
setPluginContent(null);
|
||||
} catch (e: any) {
|
||||
showError(e?.message || 'Invalid Plugin');
|
||||
}
|
||||
},
|
||||
[pluginContent, pluginBuffer],
|
||||
);
|
||||
|
||||
const onPluginInfo = useCallback(
|
||||
async (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!evt.target.files) return;
|
||||
try {
|
||||
const [file] = evt.target.files;
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const plugin = await makePlugin(arrayBuffer);
|
||||
setPluginContent(await getPluginConfig(plugin));
|
||||
setPluginBuffer(arrayBuffer);
|
||||
} catch (e: any) {
|
||||
showError(e?.message || 'Invalid Plugin');
|
||||
} finally {
|
||||
evt.target.value = '';
|
||||
}
|
||||
},
|
||||
[setPluginContent, setPluginBuffer],
|
||||
);
|
||||
|
||||
const onClose = useCallback(() => {
|
||||
setPluginContent(null);
|
||||
setPluginBuffer(null);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
className="opacity-0 absolute top-0 right-0 h-full w-full cursor-pointer"
|
||||
type="file"
|
||||
onChange={onPluginInfo}
|
||||
/>
|
||||
{error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
{pluginContent && (
|
||||
<PluginInfoModal
|
||||
pluginContent={pluginContent}
|
||||
onClose={onClose}
|
||||
onAddPlugin={onAddPlugin}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginInfoModalHeader(props: {
|
||||
className?: string;
|
||||
children: ReactNode | ReactNode[];
|
||||
}) {
|
||||
return <div className={props.className}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function PluginInfoModalContent(props: {
|
||||
className?: string;
|
||||
children: ReactNode | ReactNode[];
|
||||
}) {
|
||||
return <div className={props.className}>{props.children}</div>;
|
||||
}
|
||||
|
||||
export function PluginInfoModal(props: {
|
||||
pluginContent: PluginConfig;
|
||||
onClose: () => void;
|
||||
onAddPlugin?: MouseEventHandler;
|
||||
children?: ReactNode | ReactNode[];
|
||||
}) {
|
||||
const { pluginContent, onClose, onAddPlugin, children } = props;
|
||||
|
||||
const header = Children.toArray(children).filter(
|
||||
(c: any) => c.type.name === 'PluginInfoModalHeader',
|
||||
)[0];
|
||||
|
||||
const content = Children.toArray(children).filter(
|
||||
(c: any) => c.type.name === 'PluginInfoModalContent',
|
||||
)[0];
|
||||
|
||||
return (
|
||||
<Modal
|
||||
onClose={onClose}
|
||||
className="custom-modal !rounded-none flex items-center justify-center gap-4"
|
||||
>
|
||||
<ModalHeader className="w-full p-2 border-gray-200 text-gray-500">
|
||||
{header || (
|
||||
<div className="flex flex-row items-end justify-start gap-2">
|
||||
<img className="h-5" src={logo} alt="logo" />
|
||||
<span className="font-semibold">{`Installing ${pluginContent.title}`}</span>
|
||||
</div>
|
||||
)}
|
||||
</ModalHeader>
|
||||
<ModalContent className="flex flex-col flex-grow-0 flex-shrink-0 items-center px-8 py-2 gap-2 w-full max-h-none">
|
||||
{content || (
|
||||
<>
|
||||
<img
|
||||
className="w-12 h-12"
|
||||
src={pluginContent.icon}
|
||||
alt="Plugin Icon"
|
||||
/>
|
||||
<span className="text-3xl text-center">
|
||||
<span>
|
||||
<span className="text-blue-600 font-semibold">
|
||||
{pluginContent.title}
|
||||
</span>{' '}
|
||||
wants access to your browser
|
||||
</span>
|
||||
</span>
|
||||
</>
|
||||
)}
|
||||
</ModalContent>
|
||||
<div className="flex-grow flex-shrink overflow-y-auto w-full px-8">
|
||||
<PluginPermissions pluginContent={pluginContent} />
|
||||
</div>
|
||||
<ModalFooter className="flex justify-end gap-2 p-4">
|
||||
<button className="button" onClick={onClose}>
|
||||
Cancel
|
||||
</button>
|
||||
{onAddPlugin && (
|
||||
<button className="button button--primary" onClick={onAddPlugin}>
|
||||
Allow
|
||||
</button>
|
||||
)}
|
||||
</ModalFooter>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
export function PluginPermissions({
|
||||
pluginContent,
|
||||
className,
|
||||
}: {
|
||||
pluginContent: PluginConfig;
|
||||
className?: string;
|
||||
}) {
|
||||
return (
|
||||
<div className={classNames('flex flex-col p-2 gap-5', className)}>
|
||||
{pluginContent.hostFunctions?.map((hostFunction: string) => {
|
||||
const HFComponent = HostFunctionsDescriptions[hostFunction];
|
||||
return <HFComponent key={hostFunction} {...pluginContent} />;
|
||||
})}
|
||||
{pluginContent.cookies && (
|
||||
<PermissionDescription fa="fa-solid fa-cookie-bite">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Access cookies from</span>
|
||||
<MultipleParts parts={pluginContent.cookies} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
{pluginContent.headers && (
|
||||
<PermissionDescription fa="fa-solid fa-envelope">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Access headers from</span>
|
||||
<MultipleParts parts={pluginContent.headers} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
{pluginContent.requests && (
|
||||
<PermissionDescription fa="fa-solid fa-globe">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Submit network requests to</span>
|
||||
<MultipleParts
|
||||
parts={pluginContent?.requests.map(({ url }) => url)}
|
||||
/>
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -6,6 +6,7 @@
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
transition: 200ms opacity;
|
||||
|
||||
}
|
||||
|
||||
&:hover {
|
||||
@@ -20,4 +21,25 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.custom-modal {
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
max-width: 800px;
|
||||
max-height: 90vh;
|
||||
display: flex;
|
||||
margin: 1rem auto;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.custom-modal-content {
|
||||
flex-grow: 2;
|
||||
overflow-y: auto;
|
||||
max-height: 90%;
|
||||
}
|
||||
|
||||
.modal__overlay {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
@@ -20,6 +20,13 @@ import Icon from '../Icon';
|
||||
import './index.scss';
|
||||
import browser from 'webextension-polyfill';
|
||||
import { ErrorModal } from '../ErrorModal';
|
||||
import Modal, { ModalHeader, ModalContent, ModalFooter } from '../Modal/Modal';
|
||||
import {
|
||||
PluginInfoModal,
|
||||
PluginInfoModalContent,
|
||||
PluginInfoModalHeader,
|
||||
} from '../PluginInfo';
|
||||
import logo from '../../assets/img/icon-128.png';
|
||||
|
||||
export function PluginList(props: { className?: string }): ReactElement {
|
||||
const hashes = usePluginHashes();
|
||||
@@ -48,9 +55,11 @@ export function Plugin(props: {
|
||||
}): ReactElement {
|
||||
const [error, showError] = useState('');
|
||||
const [config, setConfig] = useState<PluginConfig | null>(null);
|
||||
const [pluginInfo, showPluginInfo] = useState(false);
|
||||
const [remove, showRemove] = useState(false);
|
||||
|
||||
const onClick = useCallback(async () => {
|
||||
if (!config) return;
|
||||
if (!config || remove) return;
|
||||
|
||||
try {
|
||||
await runPlugin(props.hash, 'start');
|
||||
@@ -69,7 +78,7 @@ export function Plugin(props: {
|
||||
} catch (e: any) {
|
||||
showError(e.message);
|
||||
}
|
||||
}, [props.hash, config]);
|
||||
}, [props.hash, config, remove]);
|
||||
|
||||
useEffect(() => {
|
||||
(async function () {
|
||||
@@ -81,33 +90,128 @@ export function Plugin(props: {
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
removePlugin(props.hash);
|
||||
showRemove(false);
|
||||
},
|
||||
[props.hash],
|
||||
[props.hash, remove],
|
||||
);
|
||||
|
||||
const onConfirmRemove: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
showRemove(true);
|
||||
},
|
||||
[props.hash, remove],
|
||||
);
|
||||
|
||||
const onPluginInfo: MouseEventHandler = useCallback(
|
||||
(e) => {
|
||||
e.stopPropagation();
|
||||
showPluginInfo(true);
|
||||
},
|
||||
[props.hash, pluginInfo],
|
||||
);
|
||||
|
||||
if (!config) return <></>;
|
||||
|
||||
return (
|
||||
<button
|
||||
<div
|
||||
className={classNames(
|
||||
'flex flex-row border rounded border-slate-300 p-2 gap-2 plugin-box',
|
||||
'flex flex-row justify-center border rounded border-slate-300 p-2 gap-2 plugin-box',
|
||||
'cursor-pointer hover:bg-slate-100 hover:border-slate-400 active:bg-slate-200',
|
||||
)}
|
||||
onClick={onClick}
|
||||
>
|
||||
{!!error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
<img className="w-12 h-12" src={config.icon || DefaultPluginIcon} />
|
||||
<div className="flex flex-col w-full items-start">
|
||||
<div className="font-bold flex flex-row h-6 items-center justify-between w-full">
|
||||
{config.title}
|
||||
<Icon
|
||||
fa="fa-solid fa-xmark"
|
||||
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
|
||||
onClick={onRemove}
|
||||
/>
|
||||
{!remove ? (
|
||||
<div className="flex flex-row w-full gap-2">
|
||||
<img className="w-12 h-12" src={config.icon || DefaultPluginIcon} />
|
||||
<div className="flex flex-col w-full items-start">
|
||||
<div className="font-bold flex flex-row h-6 items-center justify-between w-full">
|
||||
{config.title}
|
||||
<div className="flex flex-row items-center justify-center">
|
||||
<Icon
|
||||
fa="fa-solid fa-circle-info"
|
||||
className="flex flex-row items-center justify-center cursor-pointer plugin-box__remove-icon"
|
||||
onClick={onPluginInfo}
|
||||
/>
|
||||
<Icon
|
||||
fa="fa-solid fa-xmark"
|
||||
className="flex flex-row items-center justify-center cursor-pointer text-red-500 bg-red-200 rounded-full plugin-box__remove-icon"
|
||||
onClick={onConfirmRemove}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div>{config.description}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div>{config.description}</div>
|
||||
</div>
|
||||
</button>
|
||||
) : (
|
||||
<RemovePlugin
|
||||
onRemove={onRemove}
|
||||
showRemove={showRemove}
|
||||
config={config}
|
||||
/>
|
||||
)}
|
||||
{pluginInfo && (
|
||||
<PluginInfoModal
|
||||
pluginContent={config}
|
||||
onClose={() => showPluginInfo(false)}
|
||||
>
|
||||
<PluginInfoModalHeader>
|
||||
<div className="flex flex-row items-end justify-start gap-2">
|
||||
<Icon
|
||||
className="text-slate-500 hover:text-slate-700 cursor-pointer"
|
||||
size={1}
|
||||
fa="fa-solid fa-caret-left"
|
||||
onClick={() => showPluginInfo(false)}
|
||||
/>
|
||||
</div>
|
||||
</PluginInfoModalHeader>
|
||||
<PluginInfoModalContent className="flex flex-col items-center cursor-default">
|
||||
<img
|
||||
className="w-12 h-12 mb-2"
|
||||
src={config.icon}
|
||||
alt="Plugin Icon"
|
||||
/>
|
||||
<span className="text-3xl text-blue-600 font-semibold">
|
||||
{config.title}
|
||||
</span>
|
||||
<div className="text-slate-500 text-lg">{config.description}</div>
|
||||
</PluginInfoModalContent>
|
||||
</PluginInfoModal>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function RemovePlugin(props: {
|
||||
onRemove: MouseEventHandler;
|
||||
showRemove: (show: boolean) => void;
|
||||
config: PluginConfig;
|
||||
}): ReactElement {
|
||||
const { onRemove, showRemove, config } = props;
|
||||
|
||||
const onCancel: MouseEventHandler = useCallback((e) => {
|
||||
e.stopPropagation();
|
||||
showRemove(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col items-center w-full gap-1">
|
||||
<div className="font-bold text-red-700">
|
||||
{`Are you sure you want to remove "${config.title}" plugin?`}
|
||||
</div>
|
||||
<div className="mb-1">Warning: this cannot be undone.</div>
|
||||
<div className="flex flex-row w-full gap-1">
|
||||
<button className="flex-grow button p-1" onClick={onCancel}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
className="flex-grow font-bold bg-red-500 hover:bg-red-600 text-white rounded p-1"
|
||||
onClick={onRemove}
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -20,6 +20,7 @@ import History from '../../pages/History';
|
||||
import ProofUploader from '../../pages/ProofUploader';
|
||||
import browser from 'webextension-polyfill';
|
||||
import store from '../../utils/store';
|
||||
import PluginUploadInfo from '../../components/PluginInfo';
|
||||
|
||||
const Popup = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -102,6 +103,7 @@ const Popup = () => {
|
||||
<Route path="/custom/*" element={<RequestBuilder />} />
|
||||
<Route path="/options" element={<Options />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/plugininfo" element={<PluginUploadInfo />} />
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
@@ -7,13 +7,7 @@ import {
|
||||
deleteRequestHistory,
|
||||
} from '../../reducers/history';
|
||||
import Icon from '../../components/Icon';
|
||||
import {
|
||||
get,
|
||||
NOTARY_API_LS_KEY,
|
||||
PROXY_API_LS_KEY,
|
||||
getNotaryApi,
|
||||
getProxyApi,
|
||||
} from '../../utils/storage';
|
||||
import { getNotaryApi, getProxyApi } from '../../utils/storage';
|
||||
import { urlify, download, upload } from '../../utils/misc';
|
||||
import { BackgroundActiontype } from '../../entries/Background/rpc';
|
||||
import Modal, { ModalContent } from '../../components/Modal/Modal';
|
||||
@@ -211,6 +205,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
}
|
||||
|
||||
function ErrorModal(): ReactElement {
|
||||
const msg = typeof request?.error === 'string' && request?.error;
|
||||
return !showingError ? (
|
||||
<></>
|
||||
) : (
|
||||
@@ -219,7 +214,7 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
onClose={closeAllModal}
|
||||
>
|
||||
<ModalContent className="flex justify-center items-center text-slate-500">
|
||||
{request?.error || 'Something went wrong :('}
|
||||
{msg || 'Something went wrong :('}
|
||||
</ModalContent>
|
||||
<button
|
||||
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
|
||||
|
||||
@@ -1,18 +1,15 @@
|
||||
import React, {
|
||||
ChangeEvent,
|
||||
MouseEventHandler,
|
||||
ReactElement,
|
||||
ReactNode,
|
||||
useCallback,
|
||||
useState,
|
||||
} from 'react';
|
||||
import Icon from '../../components/Icon';
|
||||
import classNames from 'classnames';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useRequests } from '../../reducers/requests';
|
||||
import { makePlugin, getPluginConfig } from '../../utils/misc';
|
||||
import { addPlugin } from '../../utils/rpc';
|
||||
import { PluginList } from '../../components/PluginList';
|
||||
import PluginUploadInfo from '../../components/PluginInfo';
|
||||
import { ErrorModal } from '../../components/ErrorModal';
|
||||
|
||||
export default function Home(): ReactElement {
|
||||
@@ -20,22 +17,6 @@ export default function Home(): ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const [error, showError] = useState('');
|
||||
|
||||
const onAddPlugin = useCallback(
|
||||
async (evt: ChangeEvent<HTMLInputElement>) => {
|
||||
if (!evt.target.files) return;
|
||||
try {
|
||||
const [file] = evt.target.files;
|
||||
const arrayBuffer = await file.arrayBuffer();
|
||||
const plugin = await makePlugin(arrayBuffer);
|
||||
await getPluginConfig(plugin);
|
||||
await addPlugin(Buffer.from(arrayBuffer).toString('hex'));
|
||||
} catch (e: any) {
|
||||
showError(e?.message || 'Invalid Plugin');
|
||||
}
|
||||
},
|
||||
[],
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-4 py-4 overflow-y-auto">
|
||||
{error && <ErrorModal onClose={() => showError('')} message={error} />}
|
||||
@@ -57,11 +38,7 @@ export default function Home(): ReactElement {
|
||||
History
|
||||
</NavButton>
|
||||
<NavButton className="relative" fa="fa-solid fa-plus">
|
||||
<input
|
||||
className="opacity-0 absolute top-0 right-0 h-full w-full"
|
||||
type="file"
|
||||
onChange={onAddPlugin}
|
||||
/>
|
||||
<PluginUploadInfo />
|
||||
Add a plugin
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
|
||||
|
||||
87
src/utils/plugins.tsx
Normal file
87
src/utils/plugins.tsx
Normal file
@@ -0,0 +1,87 @@
|
||||
import { PluginConfig } from './misc';
|
||||
import React, { ReactElement, ReactNode } from 'react';
|
||||
import Icon from '../components/Icon';
|
||||
|
||||
export const HostFunctionsDescriptions: {
|
||||
[key: string]: (pluginContent: PluginConfig) => ReactElement;
|
||||
} = {
|
||||
redirect: () => {
|
||||
return (
|
||||
<PermissionDescription fa="fa-solid fa-diamond-turn-right">
|
||||
<span>Redirect your current tab to any URL</span>
|
||||
</PermissionDescription>
|
||||
);
|
||||
},
|
||||
notarize: ({ notaryUrls, proxyUrls }) => {
|
||||
const notaries = ['default notary'].concat(notaryUrls || []);
|
||||
const proxies = ['default proxy'].concat(proxyUrls || []);
|
||||
|
||||
return (
|
||||
<>
|
||||
<PermissionDescription fa="fa-solid fa-route">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Proxy notarization requests thru</span>
|
||||
<MultipleParts parts={proxies} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
<PermissionDescription fa="fa-solid fa-stamp">
|
||||
<span className="cursor-default">
|
||||
<span className="mr-1">Submit notarization requests to</span>
|
||||
<MultipleParts parts={notaries} />
|
||||
</span>
|
||||
</PermissionDescription>
|
||||
</>
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
export function PermissionDescription({
|
||||
fa,
|
||||
children,
|
||||
}: {
|
||||
fa: string;
|
||||
children?: ReactNode;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<div className="flex flex-row gap-4 items-start cursor-default">
|
||||
<Icon className="" size={1.6125} fa={fa} />
|
||||
<div className="text-sm mt-[0.125rem]">{children}</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export function MultipleParts({ parts }: { parts: string[] }): ReactElement {
|
||||
const content = [];
|
||||
|
||||
if (parts.length > 1) {
|
||||
for (let i = 0; i < parts.length; i++) {
|
||||
content.push(
|
||||
<span key={i} className="text-blue-600">
|
||||
{parts[i]}
|
||||
</span>,
|
||||
);
|
||||
|
||||
if (parts.length - i === 2) {
|
||||
content.push(
|
||||
<span key={i + 'separator'} className="inline-block mx-1">
|
||||
and
|
||||
</span>,
|
||||
);
|
||||
} else if (parts.length - i > 1) {
|
||||
content.push(
|
||||
<span key={i + 'separator'} className="inline-block mr-1">
|
||||
,
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
content.push(
|
||||
<span key={0} className="text-blue-600">
|
||||
{parts[0]}
|
||||
</span>,
|
||||
);
|
||||
}
|
||||
|
||||
return <>{content}</>;
|
||||
}
|
||||
Reference in New Issue
Block a user