diff --git a/src/components/Menu/index.tsx b/src/components/Menu/index.tsx new file mode 100644 index 00000000..2fb23f70 --- /dev/null +++ b/src/components/Menu/index.tsx @@ -0,0 +1,115 @@ +import React, { + MouseEventHandler, + ReactElement, + ReactNode, + useCallback, + useState, +} from 'react'; +import Icon from '../Icon'; +import browser from 'webextension-polyfill'; +import classNames from 'classnames'; +import { useNavigate } from 'react-router'; +import PluginUploadInfo from '../PluginInfo'; + +export function MenuIcon(): ReactElement { + const [opened, setOpen] = useState(false); + + const toggleMenu = useCallback(() => { + setOpen(!opened); + }, [opened]); + + return ( +
+ {opened && ( + <> +
+ + + )} + +
+ ); +} + +export default function Menu(props: { + opened: boolean; + setOpen: (opened: boolean) => void; +}): ReactElement { + const navigate = useNavigate(); + const openExtensionInPage = () => { + props.setOpen(false); + browser.tabs.create({ + url: `chrome-extension://${chrome.runtime.id}/popup.html`, + }); + }; + + return ( +
+
+ { + props.setOpen(false); + }} + > + + Install Plugin + + { + props.setOpen(false); + navigate('/plugins'); + }} + > + Plugins + + + Expand + + { + props.setOpen(false); + navigate('/options'); + }} + > + Options + +
+
+ ); +} + +function MenuRow(props: { + fa: string; + children?: ReactNode; + onClick?: MouseEventHandler; + className?: string; +}): ReactElement { + return ( +
+ + {props.children} +
+ ); +} diff --git a/src/components/PluginList/index.tsx b/src/components/PluginList/index.tsx index 7aaefcb4..1d871c7d 100644 --- a/src/components/PluginList/index.tsx +++ b/src/components/PluginList/index.tsx @@ -5,12 +5,7 @@ import React, { useEffect, useState, } from 'react'; -import { - fetchPluginHashes, - removePlugin, - fetchPluginConfigByHash, - runPlugin, -} from '../../utils/rpc'; +import { fetchPluginHashes, removePlugin, runPlugin } from '../../utils/rpc'; import { usePluginHashes } from '../../reducers/plugins'; import { PluginConfig } from '../../utils/misc'; import DefaultPluginIcon from '../../assets/img/default-plugin-icon.png'; diff --git a/src/components/RequestTable/index.tsx b/src/components/RequestTable/index.tsx index 4c534168..ec5f2f2d 100644 --- a/src/components/RequestTable/index.tsx +++ b/src/components/RequestTable/index.tsx @@ -1,13 +1,21 @@ -import React, { ReactElement, useCallback, useState } from 'react'; +import React, { + ReactElement, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; import { BackgroundActiontype, RequestLog } from '../../entries/Background/rpc'; import { useNavigate } from 'react-router'; import Fuse from 'fuse.js'; import Icon from '../Icon'; import { useDispatch } from 'react-redux'; import { setRequests } from '../../reducers/requests'; +import classNames from 'classnames'; type Props = { requests: RequestLog[]; + shouldFix?: boolean; }; export default function RequestTable(props: Props): ReactElement { @@ -47,7 +55,14 @@ export default function RequestTable(props: Props): ReactElement { return (
-
+
-
+
diff --git a/src/entries/Background/rpc.ts b/src/entries/Background/rpc.ts index bbbdee23..f0fb5f4b 100644 --- a/src/entries/Background/rpc.ts +++ b/src/entries/Background/rpc.ts @@ -224,7 +224,7 @@ function handleGetProveRequests( sendResponse: (data?: any) => void, ): boolean { getNotaryRequests().then(async (reqs) => { - for (const req of reqs) { + for (const req of reqs.reverse()) { await browser.runtime.sendMessage({ type: BackgroundActiontype.push_action, data: { diff --git a/src/entries/Popup/Popup.tsx b/src/entries/Popup/Popup.tsx index f8d25578..052d3425 100644 --- a/src/entries/Popup/Popup.tsx +++ b/src/entries/Popup/Popup.tsx @@ -33,6 +33,8 @@ import Icon from '../../components/Icon'; import classNames from 'classnames'; import { getConnection } from '../Background/db'; import { useIsConnected, setConnection } from '../../reducers/requests'; +import { MenuIcon } from '../../components/Menu'; +import Plugins from '../../pages/Plugins'; const Popup = () => { const dispatch = useDispatch(); @@ -84,7 +86,7 @@ const Popup = () => { }, []); return ( -
+
{ alt="logo" onClick={() => navigate('/')} /> - +
+ + +
} /> } /> } /> } /> - } /> - } /> + } /> + } /> } /> } /> + } /> } /> } /> } /> @@ -141,7 +147,7 @@ function AppConnectionLogo() { return (
setShowConnectionDetails(true)} >
diff --git a/src/entries/Popup/index.scss b/src/entries/Popup/index.scss index b8ff5d09..75e96211 100644 --- a/src/entries/Popup/index.scss +++ b/src/entries/Popup/index.scss @@ -30,6 +30,10 @@ code { width: 100vw; height: 100vh; overflow: hidden; + + @media (min-width: 1024px) { + @apply bg-slate-400; + } } .button { diff --git a/src/entries/SidePanel/SidePanel.tsx b/src/entries/SidePanel/SidePanel.tsx index 738a36cf..ba8046b1 100644 --- a/src/entries/SidePanel/SidePanel.tsx +++ b/src/entries/SidePanel/SidePanel.tsx @@ -235,7 +235,7 @@ function StepContent( )} onClick={viewProofInPopup} > - View Proof + View ); } else if (notaryRequest?.status === 'pending' || pending || notarizationId) { diff --git a/src/pages/History/index.tsx b/src/pages/History/index.tsx index f8150ab3..1fef73e1 100644 --- a/src/pages/History/index.tsx +++ b/src/pages/History/index.tsx @@ -160,7 +160,7 @@ export function OneRequestHistory(props: { className="bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100" onClick={onView} fa="fa-solid fa-receipt" - ctaText="View Proof" + ctaText="View" hidden={hideActions.includes('view')} /> (props.tab || 'history'); + const scrollableContent = useRef(null); + const [shouldFix, setFix] = useState(false); + const [actionPanelElement, setActionPanelElement] = + useState(null); + const [scrollTop, setScrollTop] = useState(0); + + useEffect(() => { + fetchPluginHashes(); + }, []); + + useEffect(() => { + const element = scrollableContent.current; + if (!element) return; + if (!actionPanelElement) return; + + let timer = Date.now(); + const onScroll = () => { + const now = Date.now(); + if (now - timer > 20) { + timer = now; + setScrollTop(element.scrollTop); + if (element.scrollTop >= actionPanelElement.clientHeight) { + setFix(true); + } else { + setFix(false); + } + } + }; + + element.addEventListener('scroll', onScroll); + + return () => { + element.removeEventListener('scroll', onScroll); + }; + }, [scrollableContent, actionPanelElement]); return ( -
+
{error && showError('')} message={error} />} -
- navigate('/requests')}> - Requests - {`(${requests.length})`} - - navigate('/custom')}> - Custom - - navigate('/verify')} + +
+ setTab('network')} + selected={tab === 'network'} + > + Network + + setTab('history')} + selected={tab === 'history'} > - Verify - - navigate('/history')}> History - - - - Add a plugin - - navigate('/options')}> - Options - + +
+
+ {tab === 'history' && } + {tab === 'network' && }
-
); } +function ActionPanel({ + setActionPanelElement, + scrollTop, +}: { + scrollTop: number; + setActionPanelElement: (el: HTMLDivElement) => void; +}) { + const pluginHashes = usePluginHashes(); + const navigate = useNavigate(); + const container = useRef(null); + const [isOverflow, setOverflow] = useState(false); + const [expanded, setExpand] = useState(false); + + useEffect(() => { + const element = container.current; + + if (!element) return; + + setActionPanelElement(element); + + const onCheckSize = () => { + if (element.scrollWidth > element.clientWidth) { + setOverflow(true); + } else { + setOverflow(false); + } + }; + + onCheckSize(); + + window.addEventListener('resize', onCheckSize); + + return () => { + window.removeEventListener('resize', onCheckSize); + }; + }, [container, pluginHashes]); + + useEffect(() => { + const element = container.current; + + if (!element) return; + + if (scrollTop >= element.clientHeight) { + setExpand(false); + } + }, [container, scrollTop]); + + return ( +
+ navigate('/custom')} + title="Build a custom request" + > + Custom + + navigate('/verify')} + title="Visualize an attestation" + > + Verify + + {pluginHashes.map((hash) => ( + + ))} + + +
+ ); +} + +function PluginIcon({ hash }: { hash: string }) { + const config = usePluginConfig(hash); + const onPluginClick = useOnPluginClick(hash); + + const onClick = useCallback(() => { + if (!config) return; + onPluginClick(); + }, [onPluginClick, config]); + + return ( + + ); +} + +function TabSelector(props: { + children: string; + className?: string; + selected?: boolean; + onClick: MouseEventHandler; +}): ReactElement { + return ( + + ); +} + function NavButton(props: { fa: string; children?: ReactNode; onClick?: MouseEventHandler; className?: string; + title?: string; disabled?: boolean; }): ReactElement { return ( diff --git a/src/pages/Options/index.tsx b/src/pages/Options/index.tsx index df37f39e..9ade6b1d 100644 --- a/src/pages/Options/index.tsx +++ b/src/pages/Options/index.tsx @@ -80,6 +80,10 @@ export default function Options(): ReactElement { setAdvanced(!advanced); }, [advanced]); + const openInTab = useCallback((url: string) => { + browser.tabs.create({ url }); + }, []); + return (
{showReloadModal && ( @@ -152,6 +156,22 @@ export default function Options(): ReactElement { Save
+
+ + +
); } diff --git a/src/pages/Plugins/index.tsx b/src/pages/Plugins/index.tsx new file mode 100644 index 00000000..c69d62ad --- /dev/null +++ b/src/pages/Plugins/index.tsx @@ -0,0 +1,10 @@ +import React, { ReactElement } from "react"; +import { PluginList } from "../../components/PluginList"; + +export default function Plugins(): ReactElement { + return ( +
+ +
+ ) +} \ No newline at end of file diff --git a/src/pages/Requests/index.tsx b/src/pages/Requests/index.tsx index f4816f86..c71636a4 100644 --- a/src/pages/Requests/index.tsx +++ b/src/pages/Requests/index.tsx @@ -2,11 +2,11 @@ import React, { ReactElement } from 'react'; import RequestTable from '../../components/RequestTable'; import { useRequests } from '../../reducers/requests'; -export default function Requests(): ReactElement { +export default function Requests(props: { shouldFix?: boolean }): ReactElement { const requests = useRequests(); return ( <> - + ); } diff --git a/src/reducers/plugins.tsx b/src/reducers/plugins.tsx index 592b4fa3..7fa12461 100644 --- a/src/reducers/plugins.tsx +++ b/src/reducers/plugins.tsx @@ -1,6 +1,11 @@ import { useSelector } from 'react-redux'; import { AppRootState } from './index'; import deepEqual from 'fast-deep-equal'; +import { useCallback, useEffect, useState } from 'react'; +import { getPluginConfigByHash } from '../entries/Background/db'; +import { PluginConfig } from '../utils/misc'; +import { runPlugin } from '../utils/rpc'; +import browser from 'webextension-polyfill'; enum ActionType { '/plugin/addPlugin' = '/plugin/addPlugin', @@ -52,3 +57,31 @@ export const usePluginHashes = (): string[] => { return state.plugins.order; }, deepEqual); }; + +export const usePluginConfig = (hash: string) => { + const [config, setConfig] = useState(null); + useEffect(() => { + (async function () { + setConfig(await getPluginConfigByHash(hash)); + })(); + }, [hash]); + return config; +}; + +export const useOnPluginClick = (hash: string) => { + return useCallback(async () => { + await runPlugin(hash, 'start'); + + const [tab] = await browser.tabs.query({ + active: true, + currentWindow: true, + }); + + await browser.storage.local.set({ plugin_hash: hash }); + + // @ts-ignore + if (chrome.sidePanel) await chrome.sidePanel.open({ tabId: tab.id }); + + window.close(); + }, [hash]); +};