mirror of
https://github.com/tlsnotary/tlsn-extension.git
synced 2026-01-10 05:28:02 -05:00
Compare commits
38 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
da2ec501f3 | ||
|
|
871c765963 | ||
|
|
31ca65ccd2 | ||
|
|
8ad4c0b4f8 | ||
|
|
3d8aec4083 | ||
|
|
e63d21e394 | ||
|
|
fca19846b4 | ||
|
|
12e6806fa8 | ||
|
|
b6d801a45e | ||
|
|
b8f67d0adf | ||
|
|
b3009023b3 | ||
|
|
02398eb253 | ||
|
|
f2e298943f | ||
|
|
743b073fbe | ||
|
|
78fc4bc452 | ||
|
|
c0fb13d5c8 | ||
|
|
6ffe529144 | ||
|
|
82d5b77969 | ||
|
|
f58431cfb2 | ||
|
|
86f21ac8a1 | ||
|
|
a42bb2eabd | ||
|
|
76c6acd998 | ||
|
|
d0024077f9 | ||
|
|
42ab67eb24 | ||
|
|
028a3b5444 | ||
|
|
d60d6a3ff3 | ||
|
|
33ad9acca5 | ||
|
|
6503281d75 | ||
|
|
5f7bc6dae0 | ||
|
|
bc4e77b8f1 | ||
|
|
8ec3e37c92 | ||
|
|
220c138290 | ||
|
|
eb505cf234 | ||
|
|
04485168e1 | ||
|
|
fec058fd7c | ||
|
|
042fba9c09 | ||
|
|
1da4f45564 | ||
|
|
c8f2b541d6 |
1
.gitignore
vendored
1
.gitignore
vendored
@@ -8,3 +8,4 @@ bin/
|
||||
build
|
||||
tlsn/
|
||||
zip
|
||||
yarn.lock
|
||||
|
||||
@@ -2,6 +2,9 @@
|
||||
|
||||
# Chrome Extension (MV3) for TLSNotary
|
||||
|
||||
### ⚠️ Notice
|
||||
- When running the extension against a [notary server](https://github.com/tlsnotary/tlsn/tree/dev/notary-server), please ensure that the server's version is the same as the version of this extension
|
||||
|
||||
## Installing and Running
|
||||
|
||||
### Procedures:
|
||||
@@ -43,4 +46,4 @@ Now, the content of `build` folder will be the extension ready to be submitted t
|
||||
## Resources:
|
||||
|
||||
- [Webpack documentation](https://webpack.js.org/concepts/)
|
||||
- [Chrome Extension documentation](https://developer.chrome.com/extensions/getstarted)
|
||||
- [Chrome Extension documentation](https://developer.chrome.com/extensions/getstarted)
|
||||
|
||||
33
cog.toml
Normal file
33
cog.toml
Normal file
@@ -0,0 +1,33 @@
|
||||
branch_whitelist = []
|
||||
disable_changelog = false
|
||||
from_latest_tag = false
|
||||
generate_mono_repository_global_tag = true
|
||||
ignore_merge_commits = false
|
||||
post_bump_hooks = []
|
||||
post_package_bump_hooks = []
|
||||
pre_bump_hooks = [
|
||||
"echo {{version}}",
|
||||
]
|
||||
pre_package_bump_hooks = []
|
||||
skip_ci = "[skip ci]"
|
||||
skip_untracked = false
|
||||
|
||||
[git_hooks]
|
||||
|
||||
[commit_types]
|
||||
|
||||
[changelog]
|
||||
authors = [
|
||||
{username = "0xtsukino", signature = "tsukino"},
|
||||
{username = "heeckhau", signature = "Hendrik Eeckhaut"},
|
||||
{username = "mhchia", signature = "Kevin Mai-Husan Chia"},
|
||||
]
|
||||
owner = "TLSNotary"
|
||||
path = "CHANGELOG.md"
|
||||
remote = "github.com"
|
||||
repository = "tlsn-extension"
|
||||
template = "remote"
|
||||
|
||||
[bump_profiles]
|
||||
|
||||
[packages]
|
||||
8257
package-lock.json
generated
8257
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "tlsn-extension",
|
||||
"version": "0.1.0.3",
|
||||
"version": "0.1.0.4",
|
||||
"license": "MIT",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -22,6 +22,7 @@
|
||||
"charwise": "^3.0.1",
|
||||
"classnames": "^2.3.2",
|
||||
"comlink": "^4.4.1",
|
||||
"copy-to-clipboard": "^3.3.3",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"fuse.js": "^6.6.2",
|
||||
"level": "^8.0.0",
|
||||
@@ -35,7 +36,7 @@
|
||||
"redux-logger": "^3.0.6",
|
||||
"redux-thunk": "^2.4.2",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"tlsn-js": "0.1.0-alpha.3-rc1"
|
||||
"tlsn-js": "0.1.0-alpha.4.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@babel/core": "^7.20.12",
|
||||
@@ -86,6 +87,7 @@
|
||||
"webpack": "^5.75.0",
|
||||
"webpack-cli": "^4.10.0",
|
||||
"webpack-dev-server": "^4.11.1",
|
||||
"webpack-ext-reloader": "^1.1.12",
|
||||
"zip-webpack-plugin": "^4.0.1"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
8630
pnpm-lock.yaml
generated
Normal file
8630
pnpm-lock.yaml
generated
Normal file
File diff suppressed because it is too large
Load Diff
114
src/components/ChatBox/index.tsx
Normal file
114
src/components/ChatBox/index.tsx
Normal file
@@ -0,0 +1,114 @@
|
||||
import React, { useCallback, useEffect, useState } from 'react';
|
||||
import {
|
||||
sendChat,
|
||||
useChatMessages,
|
||||
useClientId,
|
||||
usePairId,
|
||||
} from '../../reducers/p2p';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import classNames from 'classnames';
|
||||
import Icon from '../Icon';
|
||||
|
||||
export default function ChatBox() {
|
||||
const messages = useChatMessages();
|
||||
const dispatch = useDispatch();
|
||||
const clientId = useClientId();
|
||||
const [text, setText] = useState('');
|
||||
const pairId = usePairId();
|
||||
|
||||
const onSend = useCallback(() => {
|
||||
if (text && pairId) {
|
||||
dispatch(
|
||||
sendChat({
|
||||
text,
|
||||
from: clientId,
|
||||
to: pairId,
|
||||
}),
|
||||
);
|
||||
|
||||
setText('');
|
||||
|
||||
console.log('after sending');
|
||||
}
|
||||
}, [text, pairId]);
|
||||
|
||||
const handleKeyDown = useCallback(
|
||||
(e: React.KeyboardEvent) => {
|
||||
if (e.key === 'Enter' && text && pairId) {
|
||||
onSend();
|
||||
setText('');
|
||||
}
|
||||
},
|
||||
[text],
|
||||
);
|
||||
const isClient = (msg: any) => {
|
||||
return msg.from === clientId;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow gap-1 p-2 flex-shrink h-0 ">
|
||||
<div className="flex flex-row gap-1 font-semibold text-xs align-center">
|
||||
<div>Client ID:</div>
|
||||
{clientId ? (
|
||||
<div className="text-green-500">{clientId}</div>
|
||||
) : (
|
||||
<Icon
|
||||
className="animate-spin text-gray-500"
|
||||
fa="fa-solid fa-spinner"
|
||||
size={1}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-row gap-1 font-semibold text-xs align-center">
|
||||
<div>Peer ID:</div>
|
||||
{pairId ? (
|
||||
<div className="text-red-500">{pairId}</div>
|
||||
) : (
|
||||
<div className="flex flex-row gap-1">
|
||||
<span className="text-slate-500">Waiting for Peer</span>
|
||||
<Icon
|
||||
className="animate-spin text-slate-500 w-fit"
|
||||
fa="fa-solid fa-spinner"
|
||||
size={1}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex flex-col flex-grow flex-shrink h-0 gap-1">
|
||||
<div className="flex flex-col border gap-1 border-slate-200 flex-grow overflow-y-auto">
|
||||
{messages.map((msg) => {
|
||||
return (
|
||||
<div
|
||||
className={`rounded-lg p-2 max-w-[50%] break-all ${isClient(msg) ? 'mr-auto bg-blue-600' : 'ml-auto bg-slate-300'}`}
|
||||
>
|
||||
<div
|
||||
className={`${isClient(msg) ? 'text-white' : 'text-black'}`}
|
||||
>
|
||||
{msg.text}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<div className="flex flex-row w-full gap-1">
|
||||
<input
|
||||
className="input border border-slate-200 focus:border-slate-400 flex-grow p-2"
|
||||
onChange={(e) => setText(e.target.value)}
|
||||
onKeyDown={handleKeyDown}
|
||||
value={text}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
className={classNames('button', {
|
||||
'button--primary': !!text && !!pairId,
|
||||
})}
|
||||
disabled={!text || !pairId}
|
||||
onClick={onSend}
|
||||
>
|
||||
Send
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -84,7 +84,7 @@ export function ModalFooter(props: FooterProps): ReactElement {
|
||||
return (
|
||||
<div
|
||||
className={classNames(
|
||||
'border-t modal__footer border-gray-100',
|
||||
'border-t modal__footer border-gray-100 w-full',
|
||||
props.className,
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -177,10 +177,19 @@ async function handleRetryProveReqest(
|
||||
) {
|
||||
const { id, notaryUrl, websocketProxyUrl } = request.data;
|
||||
|
||||
await setNotaryRequestError(id, null);
|
||||
await setNotaryRequestStatus(id, 'pending');
|
||||
|
||||
const req = await getNotaryRequest(id);
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.push_action,
|
||||
data: {
|
||||
tabId: 'background',
|
||||
},
|
||||
action: addRequestHistory(req),
|
||||
});
|
||||
|
||||
await browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.process_prove_request,
|
||||
data: {
|
||||
|
||||
@@ -45,6 +45,8 @@ const Offscreen = () => {
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
console.log('i caught an error');
|
||||
console.error(error);
|
||||
browser.runtime.sendMessage({
|
||||
type: BackgroundActiontype.finish_prove_request,
|
||||
data: {
|
||||
|
||||
@@ -18,7 +18,10 @@ import Notarize from '../../pages/Notarize';
|
||||
import ProofViewer from '../../pages/ProofViewer';
|
||||
import History from '../../pages/History';
|
||||
import ProofUploader from '../../pages/ProofUploader';
|
||||
import Connect from '../../pages/Connect';
|
||||
import browser from 'webextension-polyfill';
|
||||
import P2P from '../../pages/P2P';
|
||||
import CreateSession from '../../pages/CreateSession';
|
||||
|
||||
const Popup = () => {
|
||||
const dispatch = useDispatch();
|
||||
@@ -79,6 +82,9 @@ const Popup = () => {
|
||||
<Route path="/custom/*" element={<RequestBuilder />} />
|
||||
<Route path="/options" element={<Options />} />
|
||||
<Route path="/home" element={<Home />} />
|
||||
<Route path="/connect-session" element={<Connect />} />
|
||||
<Route path="/create-session" element={<CreateSession />} />
|
||||
<Route path="/p2p" element={<P2P />} />
|
||||
<Route path="*" element={<Navigate to="/home" />} />
|
||||
</Routes>
|
||||
</div>
|
||||
|
||||
64
src/pages/Connect/index.tsx
Normal file
64
src/pages/Connect/index.tsx
Normal file
@@ -0,0 +1,64 @@
|
||||
import React, { useEffect, useState, useCallback } from 'react';
|
||||
import { sendPairRequest } from '../../reducers/p2p';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { usePairId } from '../../reducers/p2p';
|
||||
import Icon from '../../components/Icon';
|
||||
|
||||
export default function Connect() {
|
||||
const dispatch = useDispatch();
|
||||
const [peerId, setPeerId] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
const pairId = usePairId();
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => {
|
||||
if (pairId && loading) {
|
||||
console.log('Connected to peer', pairId);
|
||||
setLoading(false);
|
||||
navigate('/create-session');
|
||||
}
|
||||
}, [pairId]);
|
||||
|
||||
const connect = useCallback(() => {
|
||||
if (peerId) {
|
||||
console.log('Connecting to peer', peerId);
|
||||
dispatch(sendPairRequest(peerId));
|
||||
setLoading(true);
|
||||
} else {
|
||||
console.log('No peer ID provided');
|
||||
}
|
||||
}, [peerId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col justify-center items-center bg-slate-200 p-4 rounded border-slate-400 m-4 gap-2">
|
||||
<h1 className="text-base font-semibold">Enter peer ID to connect to</h1>
|
||||
<input
|
||||
className="input border border-slate-200 focus:border-slate-400 w-full"
|
||||
type="text"
|
||||
value={peerId}
|
||||
onChange={(e) => setPeerId(e.target.value)}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Enter') connect();
|
||||
}}
|
||||
autoFocus
|
||||
/>
|
||||
<button
|
||||
className="button button--primary"
|
||||
disabled={!peerId || loading}
|
||||
onClick={connect}
|
||||
>
|
||||
{loading ? (
|
||||
<Icon
|
||||
className="animate-spin text-white"
|
||||
fa="fa-solid fa-spinner"
|
||||
size={1}
|
||||
/>
|
||||
) : (
|
||||
'Connect'
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
7
src/pages/CreateSession/index.tsx
Normal file
7
src/pages/CreateSession/index.tsx
Normal file
@@ -0,0 +1,7 @@
|
||||
import type {} from 'redux-thunk/extend-redux';
|
||||
import React, { ReactElement, useEffect } from 'react';
|
||||
import ChatBox from '../../components/ChatBox';
|
||||
|
||||
export default function CreateSession(): ReactElement {
|
||||
return <ChatBox />;
|
||||
}
|
||||
@@ -8,8 +8,12 @@ import {
|
||||
} from '../../reducers/history';
|
||||
import Icon from '../../components/Icon';
|
||||
import { get, NOTARY_API_LS_KEY, PROXY_API_LS_KEY } from '../../utils/storage';
|
||||
import { urlify, download } from '../../utils/misc';
|
||||
import { urlify, download, upload } from '../../utils/misc';
|
||||
import { BackgroundActiontype } from '../../entries/Background/rpc';
|
||||
import Modal, { ModalContent } from '../../components/Modal/Modal';
|
||||
import classNames from 'classnames';
|
||||
import copy from 'copy-to-clipboard';
|
||||
import { EXPLORER_API } from '../../utils/constants';
|
||||
|
||||
export default function History(): ReactElement {
|
||||
const history = useHistoryOrder();
|
||||
@@ -26,6 +30,12 @@ export default function History(): ReactElement {
|
||||
function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
const dispatch = useDispatch();
|
||||
const request = useRequestHistory(props.requestId);
|
||||
const [showingError, showError] = useState(false);
|
||||
const [uploadError, setUploadError] = useState('');
|
||||
const [showingShareConfirmation, setShowingShareConfirmation] =
|
||||
useState(false);
|
||||
const [cid, setCid] = useState('');
|
||||
const [uploading, setUploading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
const { status } = request || {};
|
||||
const requestUrl = urlify(request?.url || '');
|
||||
@@ -55,8 +65,34 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
dispatch(deleteRequestHistory(props.requestId));
|
||||
}, [props.requestId]);
|
||||
|
||||
const onShowError = useCallback(async () => {
|
||||
showError(true);
|
||||
}, [request?.error, showError]);
|
||||
|
||||
const closeAllModal = useCallback(() => {
|
||||
setShowingShareConfirmation(false);
|
||||
showError(false);
|
||||
}, [setShowingShareConfirmation, showError]);
|
||||
|
||||
const handleUpload = useCallback(async () => {
|
||||
setUploading(true);
|
||||
try {
|
||||
const data = await upload(
|
||||
`${request?.id}.json`,
|
||||
JSON.stringify(request?.proof),
|
||||
);
|
||||
setCid(data);
|
||||
} catch (e: any) {
|
||||
setUploadError(e.message);
|
||||
} finally {
|
||||
setUploading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-row flex-nowrap border rounded-md p-2 gap-1 hover:bg-slate-50 cursor-pointer">
|
||||
<ShareConfirmationModal />
|
||||
<ErrorModal />
|
||||
<div className="flex flex-col flex-nowrap flex-grow flex-shrink w-0">
|
||||
<div className="flex flex-row items-center text-xs">
|
||||
<div className="bg-slate-200 text-slate-400 px-1 py-0.5 rounded-sm">
|
||||
@@ -84,47 +120,179 @@ function OneRequestHistory(props: { requestId: string }): ReactElement {
|
||||
<div className="flex flex-col gap-1">
|
||||
{status === 'success' && (
|
||||
<>
|
||||
<div
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 hover:font-bold"
|
||||
<ActionButton
|
||||
className="bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100"
|
||||
onClick={onView}
|
||||
>
|
||||
<Icon className="" fa="fa-solid fa-receipt" size={1} />
|
||||
<span className="text-xs font-bold">View Proof</span>
|
||||
</div>
|
||||
<div
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
|
||||
fa="fa-solid fa-receipt"
|
||||
ctaText="View Proof"
|
||||
/>
|
||||
<ActionButton
|
||||
className="bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500"
|
||||
onClick={() =>
|
||||
download(`${request?.id}.json`, JSON.stringify(request?.proof))
|
||||
}
|
||||
>
|
||||
<Icon className="" fa="fa-solid fa-download" size={1} />
|
||||
<span className="text-xs font-bold">Download</span>
|
||||
</div>
|
||||
fa="fa-solid fa-download"
|
||||
ctaText="Download"
|
||||
/>
|
||||
<ActionButton
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
|
||||
onClick={() => setShowingShareConfirmation(true)}
|
||||
fa="fa-solid fa-upload"
|
||||
ctaText="Share"
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{(!status || status === 'error') && (
|
||||
<div
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
|
||||
onClick={onRetry}
|
||||
>
|
||||
<Icon fa="fa-solid fa-arrows-rotate" size={1} />
|
||||
<span className="text-xs font-bold">Retry</span>
|
||||
</div>
|
||||
)}
|
||||
{status === 'error' && !!request?.error && <ErrorButton />}
|
||||
{(!status || status === 'error') && <RetryButton />}
|
||||
{status === 'pending' && (
|
||||
<div className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
|
||||
<button className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 font-bold">
|
||||
<Icon className="animate-spin" fa="fa-solid fa-spinner" size={1} />
|
||||
<span className="text-xs font-bold">Pending</span>
|
||||
</div>
|
||||
</button>
|
||||
)}
|
||||
<div
|
||||
<button
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-red-100 hover:text-red-500 hover:font-bold"
|
||||
onClick={onDelete}
|
||||
>
|
||||
<Icon className="" fa="fa-solid fa-trash" size={1} />
|
||||
<span className="text-xs font-bold">Delete</span>
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
function RetryButton(): ReactElement {
|
||||
return (
|
||||
<button
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-slate-100 text-slate-300 hover:bg-slate-200 hover:text-slate-500 hover:font-bold"
|
||||
onClick={onRetry}
|
||||
>
|
||||
<Icon fa="fa-solid fa-arrows-rotate" size={1} />
|
||||
<span className="text-xs font-bold">Retry</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorButton(): ReactElement {
|
||||
return (
|
||||
<button
|
||||
className="flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500 hover:font-bold"
|
||||
onClick={onShowError}
|
||||
>
|
||||
<Icon fa="fa-solid fa-circle-exclamation" size={1} />
|
||||
<span className="text-xs font-bold">Error</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
function ErrorModal(): ReactElement {
|
||||
return !showingError ? (
|
||||
<></>
|
||||
) : (
|
||||
<Modal
|
||||
className="flex flex-col gap-4 items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] min-h-24 p-4 border border-red-500"
|
||||
onClose={closeAllModal}
|
||||
>
|
||||
<ModalContent className="flex justify-center items-center text-slate-500">
|
||||
{request?.error || 'Something went wrong :('}
|
||||
</ModalContent>
|
||||
<button
|
||||
className="m-0 w-24 bg-red-100 text-red-300 hover:bg-red-200 hover:text-red-500"
|
||||
onClick={closeAllModal}
|
||||
>
|
||||
OK
|
||||
</button>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
|
||||
function ShareConfirmationModal(): ReactElement {
|
||||
return !showingShareConfirmation ? (
|
||||
<></>
|
||||
) : (
|
||||
<Modal
|
||||
className="flex flex-col items-center text-base cursor-default justify-center !w-auto mx-4 my-[50%] p-4 gap-4"
|
||||
onClose={closeAllModal}
|
||||
>
|
||||
<ModalContent className="flex flex-col w-full gap-4 items-center text-base justify-center">
|
||||
{!cid ? (
|
||||
<p className="text-slate-500 text-center">
|
||||
{uploadError ||
|
||||
'This will make your proof publicly accessible by anyone with the CID'}
|
||||
</p>
|
||||
) : (
|
||||
<input
|
||||
className="input w-full bg-slate-100 border border-slate-200"
|
||||
readOnly
|
||||
value={`${EXPLORER_API}/ipfs/${cid}`}
|
||||
onFocus={(e) => e.target.select()}
|
||||
/>
|
||||
)}
|
||||
</ModalContent>
|
||||
<div className="flex flex-row gap-2 justify-center">
|
||||
{!cid ? (
|
||||
<>
|
||||
{!uploadError && (
|
||||
<button
|
||||
onClick={handleUpload}
|
||||
className="button button--primary flex flex-row items-center justify-center gap-2 m-0"
|
||||
disabled={uploading}
|
||||
>
|
||||
{uploading && (
|
||||
<Icon
|
||||
className="animate-spin"
|
||||
fa="fa-solid fa-spinner"
|
||||
size={1}
|
||||
/>
|
||||
)}
|
||||
I understand
|
||||
</button>
|
||||
)}
|
||||
<button
|
||||
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
|
||||
onClick={closeAllModal}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
onClick={() => copy(`${EXPLORER_API}/ipfs/${cid}`)}
|
||||
className="m-0 w-24 bg-slate-600 text-slate-200 hover:bg-slate-500 hover:text-slate-100 font-bold"
|
||||
>
|
||||
Copy
|
||||
</button>
|
||||
<button
|
||||
className="m-0 w-24 bg-slate-100 text-slate-400 hover:bg-slate-200 hover:text-slate-600 font-bold"
|
||||
onClick={closeAllModal}
|
||||
>
|
||||
Close
|
||||
</button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</Modal>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function ActionButton(props: {
|
||||
onClick: () => void;
|
||||
fa: string;
|
||||
ctaText: string;
|
||||
className?: string;
|
||||
}): ReactElement {
|
||||
return (
|
||||
<button
|
||||
className={classNames(
|
||||
'flex flex-row flex-grow-0 gap-2 self-end items-center justify-end px-2 py-1 hover:font-bold',
|
||||
props.className,
|
||||
)}
|
||||
onClick={props.onClick}
|
||||
>
|
||||
<Icon className="" fa={props.fa} size={1} />
|
||||
<span className="text-xs font-bold">{props.ctaText}</span>
|
||||
</button>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -50,6 +50,9 @@ export default function Home(): ReactElement {
|
||||
<NavButton fa="fa-solid fa-gear" onClick={() => navigate('/options')}>
|
||||
Options
|
||||
</NavButton>
|
||||
<NavButton fa="fa-solid fa-wifi" onClick={() => navigate('/p2p')}>
|
||||
Prove to peer
|
||||
</NavButton>
|
||||
</div>
|
||||
{!bookmarks.length && (
|
||||
<div className="flex flex-col flex-nowrap">
|
||||
|
||||
@@ -9,6 +9,9 @@ import {
|
||||
export default function Options(): ReactElement {
|
||||
const [notary, setNotary] = useState('https://notary.pse.dev');
|
||||
const [proxy, setProxy] = useState('wss://notary.pse.dev/proxy');
|
||||
const [rendezvous, setRendezvous] = useState(
|
||||
'wss://notary.pse.dev/rendezvous',
|
||||
);
|
||||
const [dirty, setDirty] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
@@ -55,6 +58,19 @@ export default function Options(): ReactElement {
|
||||
value={proxy}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-col flex-nowrap py-1 px-2 gap-2">
|
||||
<div className="font-semibold">Rendezvous API</div>
|
||||
<input
|
||||
type="text"
|
||||
className="input border"
|
||||
placeholder="wss://notary.pse.dev/rendezvous"
|
||||
onChange={(e) => {
|
||||
setRendezvous(e.target.value);
|
||||
setDirty(true);
|
||||
}}
|
||||
value={rendezvous}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex flex-row flex-nowrap justify-end gap-2 p-2">
|
||||
<button
|
||||
className="button !bg-primary/[0.9] hover:bg-primary/[0.8] active:bg-primary !text-white"
|
||||
|
||||
30
src/pages/P2P/index.tsx
Normal file
30
src/pages/P2P/index.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
import React, { ReactElement, useEffect } from 'react';
|
||||
import { useNavigate } from 'react-router';
|
||||
import { useDispatch } from 'react-redux';
|
||||
import { connectSession } from '../../reducers/p2p';
|
||||
|
||||
export default function P2P(): ReactElement {
|
||||
const navigate = useNavigate();
|
||||
const dispatch = useDispatch();
|
||||
|
||||
useEffect(() => {
|
||||
dispatch(connectSession());
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col flex-nowrap flex-grow gap-2 m-4">
|
||||
<button
|
||||
className="button button--primary mx-20 brea"
|
||||
onClick={() => navigate('/create-session')}
|
||||
>
|
||||
Create Session
|
||||
</button>
|
||||
<button
|
||||
className="button button--primary mx-20"
|
||||
onClick={() => navigate('/connect-session')}
|
||||
>
|
||||
Connect to Session
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import React, {
|
||||
useState,
|
||||
MouseEventHandler,
|
||||
} from 'react';
|
||||
import { useParams, useLocation, useNavigate } from 'react-router';
|
||||
import { useParams, useNavigate } from 'react-router';
|
||||
import c from 'classnames';
|
||||
import { useRequestHistory } from '../../reducers/history';
|
||||
import Icon from '../../components/Icon';
|
||||
|
||||
@@ -1,10 +1,12 @@
|
||||
import { combineReducers } from 'redux';
|
||||
import requests from './requests';
|
||||
import history from './history';
|
||||
import p2p from './p2p';
|
||||
|
||||
const rootReducer = combineReducers({
|
||||
requests,
|
||||
history,
|
||||
p2p,
|
||||
});
|
||||
|
||||
export type AppRootState = ReturnType<typeof rootReducer>;
|
||||
|
||||
264
src/reducers/p2p.tsx
Normal file
264
src/reducers/p2p.tsx
Normal file
@@ -0,0 +1,264 @@
|
||||
import { useSelector } from 'react-redux';
|
||||
import { AppRootState } from './index';
|
||||
import deepEqual from 'fast-deep-equal';
|
||||
import { safeParseJSON } from '../utils/misc';
|
||||
import { Dispatch } from 'redux';
|
||||
|
||||
enum ActionType {
|
||||
'/p2p/createSession' = '/p2p/createSession',
|
||||
'/p2p/setConnected' = '/p2p/setConnected',
|
||||
'/p2p/setClientId' = '/p2p/setClientId',
|
||||
'/p2p/setSocket' = '/p2p/setSocket',
|
||||
'/p2p/appendMessage' = '/p2p/appendMessage',
|
||||
'/p2p/setMessages' = '/p2p/setMessages',
|
||||
'/p2p/setPairing' = '/p2p/setPairing',
|
||||
}
|
||||
|
||||
type Action<payload> = {
|
||||
type: ActionType;
|
||||
payload?: payload;
|
||||
error?: boolean;
|
||||
meta?: any;
|
||||
};
|
||||
|
||||
type State = {
|
||||
clientId: string;
|
||||
pairing: string;
|
||||
socket: WebSocket | null;
|
||||
connected: boolean;
|
||||
messages: Chat[];
|
||||
};
|
||||
|
||||
type Chat = {
|
||||
to: string;
|
||||
from: string;
|
||||
text: string;
|
||||
};
|
||||
|
||||
const initialState: State = {
|
||||
clientId: '',
|
||||
pairing: '',
|
||||
socket: null,
|
||||
connected: false,
|
||||
messages: [],
|
||||
};
|
||||
|
||||
export const connectSession =
|
||||
() => async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
const { p2p } = getState();
|
||||
|
||||
if (p2p.socket) return;
|
||||
|
||||
const socket = new WebSocket('ws://0.tcp.ngrok.io:14339?clientId=hi');
|
||||
|
||||
socket.onopen = () => {
|
||||
console.log('Connected to websocket');
|
||||
dispatch(setConnected(true));
|
||||
dispatch(setSocket(socket));
|
||||
};
|
||||
|
||||
socket.onmessage = async (event) => {
|
||||
const message: any = safeParseJSON(await event.data.text());
|
||||
|
||||
console.log(message);
|
||||
switch (message.method) {
|
||||
case 'client_connect': {
|
||||
const { clientId } = message.params;
|
||||
dispatch(setClientId(clientId));
|
||||
break;
|
||||
}
|
||||
case 'chat': {
|
||||
const { to, from, text } = message.params;
|
||||
dispatch(
|
||||
appendMessage({
|
||||
to,
|
||||
from,
|
||||
text,
|
||||
}),
|
||||
);
|
||||
break;
|
||||
}
|
||||
case 'pair_request': {
|
||||
const { from } = message.params;
|
||||
dispatch(confirmPairRequest(from));
|
||||
break;
|
||||
}
|
||||
case 'pair_request_success': {
|
||||
const { from } = message.params;
|
||||
dispatch(setPairing(from));
|
||||
break;
|
||||
}
|
||||
default:
|
||||
console.warn(`Unknown message type "${message.method}"`);
|
||||
break;
|
||||
}
|
||||
};
|
||||
socket.onerror = () => {
|
||||
console.error('Error connecting to websocket');
|
||||
dispatch(setConnected(false));
|
||||
};
|
||||
};
|
||||
|
||||
export const setConnected = (connected = false) => ({
|
||||
type: ActionType['/p2p/setConnected'],
|
||||
payload: connected,
|
||||
});
|
||||
|
||||
export const setClientId = (clientId: string) => ({
|
||||
type: ActionType['/p2p/setClientId'],
|
||||
payload: clientId,
|
||||
});
|
||||
|
||||
export const setSocket = (socket: WebSocket) => ({
|
||||
type: ActionType['/p2p/setSocket'],
|
||||
payload: socket,
|
||||
});
|
||||
|
||||
export const setMessages = (messages: Chat[]) => ({
|
||||
type: ActionType['/p2p/setMessages'],
|
||||
payload: messages,
|
||||
});
|
||||
|
||||
export const appendMessage = (message: Chat) => ({
|
||||
type: ActionType['/p2p/appendMessage'],
|
||||
payload: message,
|
||||
});
|
||||
|
||||
export const setPairing = (clientId: string) => ({
|
||||
type: ActionType['/p2p/setPairing'],
|
||||
payload: clientId,
|
||||
});
|
||||
|
||||
let id = 1;
|
||||
export const sendChat =
|
||||
(message: Chat) =>
|
||||
async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
const {
|
||||
p2p: { socket },
|
||||
} = getState();
|
||||
if (socket) {
|
||||
socket.send(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
method: 'chat',
|
||||
params: {
|
||||
...message,
|
||||
id: id++,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
dispatch(appendMessage(message));
|
||||
}
|
||||
};
|
||||
export const sendPairRequest =
|
||||
(target: string) =>
|
||||
async (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
const {
|
||||
p2p: { socket, clientId },
|
||||
} = getState();
|
||||
if (socket && clientId) {
|
||||
socket.send(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
method: 'pair_request',
|
||||
params: {
|
||||
from: clientId,
|
||||
to: target,
|
||||
id: id++,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
}
|
||||
};
|
||||
export const confirmPairRequest =
|
||||
(target: string) => (dispatch: Dispatch, getState: () => AppRootState) => {
|
||||
const {
|
||||
p2p: { socket, clientId },
|
||||
} = getState();
|
||||
|
||||
console.log({ target, clientId });
|
||||
if (socket && clientId) {
|
||||
socket.send(
|
||||
Buffer.from(
|
||||
JSON.stringify({
|
||||
method: 'pair_request_success',
|
||||
params: {
|
||||
from: clientId,
|
||||
to: target,
|
||||
id: id++,
|
||||
},
|
||||
}),
|
||||
),
|
||||
);
|
||||
dispatch(setPairing(target));
|
||||
}
|
||||
};
|
||||
|
||||
export default function p2p(state = initialState, action: Action<any>) {
|
||||
switch (action.type) {
|
||||
case ActionType['/p2p/setConnected']:
|
||||
return {
|
||||
...state,
|
||||
connected: action.payload,
|
||||
};
|
||||
case ActionType['/p2p/setClientId']:
|
||||
return {
|
||||
...state,
|
||||
clientId: action.payload,
|
||||
};
|
||||
case ActionType['/p2p/setSocket']:
|
||||
return {
|
||||
...state,
|
||||
socket: action.payload,
|
||||
};
|
||||
case ActionType['/p2p/setMessages']:
|
||||
return {
|
||||
...state,
|
||||
messages: action.payload,
|
||||
};
|
||||
case ActionType['/p2p/setPairing']:
|
||||
return {
|
||||
...state,
|
||||
pairing: action.payload,
|
||||
};
|
||||
case ActionType['/p2p/appendMessage']:
|
||||
return {
|
||||
...state,
|
||||
messages: state.messages.concat(action.payload),
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
|
||||
export function useClientId() {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.p2p.clientId;
|
||||
}, deepEqual);
|
||||
}
|
||||
|
||||
export function useSocket() {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.p2p.socket;
|
||||
}, deepEqual);
|
||||
}
|
||||
|
||||
export function useConnected() {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.p2p.connected;
|
||||
}, deepEqual);
|
||||
}
|
||||
|
||||
export function useChatMessages(): Chat[] {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.p2p.messages;
|
||||
}, deepEqual);
|
||||
}
|
||||
|
||||
export function usePairId(): string {
|
||||
return useSelector((state: AppRootState) => {
|
||||
return state.p2p.pairing;
|
||||
}, deepEqual);
|
||||
}
|
||||
1
src/utils/constants.ts
Normal file
1
src/utils/constants.ts
Normal file
@@ -0,0 +1 @@
|
||||
export const EXPLORER_API = 'http://localhost:3000';
|
||||
@@ -1,4 +1,5 @@
|
||||
import { RequestLog } from '../entries/Background/rpc';
|
||||
import { EXPLORER_API } from './constants';
|
||||
|
||||
export function urlify(
|
||||
text: string,
|
||||
@@ -41,6 +42,33 @@ export function download(filename: string, content: string) {
|
||||
document.body.removeChild(element);
|
||||
}
|
||||
|
||||
export async function upload(filename: string, content: string) {
|
||||
const formData = new FormData();
|
||||
|
||||
formData.append(
|
||||
'file',
|
||||
new Blob([content], { type: 'application/json' }),
|
||||
filename,
|
||||
);
|
||||
const response = await fetch(`${EXPLORER_API}/api/upload`, {
|
||||
method: 'POST',
|
||||
body: formData,
|
||||
});
|
||||
if (!response.ok) {
|
||||
throw new Error('Failed to upload');
|
||||
}
|
||||
const data = await response.json();
|
||||
return data;
|
||||
}
|
||||
|
||||
export const copyText = async (text: string): Promise<void> => {
|
||||
try {
|
||||
await navigator.clipboard.writeText(text);
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
}
|
||||
};
|
||||
|
||||
export async function replayRequest(req: RequestLog): Promise<string> {
|
||||
const options = {
|
||||
method: req.method,
|
||||
@@ -80,3 +108,11 @@ export async function replayRequest(req: RequestLog): Promise<string> {
|
||||
return resp.blob().then((blob) => blob.text());
|
||||
}
|
||||
}
|
||||
|
||||
export function safeParseJSON(data: string) {
|
||||
try {
|
||||
return JSON.parse(data);
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
// tiny wrapper with default env vars
|
||||
module.exports = {
|
||||
NODE_ENV: process.env.NODE_ENV || 'development',
|
||||
PORT: process.env.PORT || 3000,
|
||||
PORT: process.env.PORT || 3001,
|
||||
};
|
||||
|
||||
@@ -8,6 +8,7 @@ var webpack = require("webpack"),
|
||||
var { CleanWebpackPlugin } = require("clean-webpack-plugin");
|
||||
var ReactRefreshWebpackPlugin = require("@pmmmwh/react-refresh-webpack-plugin");
|
||||
var ReactRefreshTypeScript = require("react-refresh-typescript");
|
||||
var ExtReloader = require('webpack-ext-reloader');
|
||||
|
||||
const ASSET_PATH = process.env.ASSET_PATH || "/";
|
||||
|
||||
@@ -145,6 +146,9 @@ var options = {
|
||||
new webpack.ProgressPlugin(),
|
||||
// expose and write the allowed env vars on the compiled bundle
|
||||
new webpack.EnvironmentPlugin(["NODE_ENV"]),
|
||||
new ExtReloader({
|
||||
manifest: path.resolve(__dirname, "src/manifest.json")
|
||||
}),
|
||||
new CopyWebpackPlugin({
|
||||
patterns: [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user