Compare commits

...

38 Commits

Author SHA1 Message Date
John Shaw
da2ec501f3 chatbox fix 2024-04-13 18:15:58 +02:00
tsukino
871c765963 fix: auto scroll messages 2024-04-13 18:13:54 +02:00
John Shaw
31ca65ccd2 chatbox styling 2024-04-13 17:48:31 +02:00
John Shaw
8ad4c0b4f8 wip 2024-04-13 16:52:06 +02:00
tsukino
3d8aec4083 fix: styling change 2024-04-13 16:51:27 +02:00
tsukino
e63d21e394 fix: use correct method name 2024-04-13 16:40:44 +02:00
John Shaw
fca19846b4 wip 2024-04-13 16:30:45 +02:00
tsukino
12e6806fa8 wip: move create session to p2p 2024-04-13 16:27:53 +02:00
tsukino
b6d801a45e wip 2024-04-13 16:27:04 +02:00
John Shaw
b8f67d0adf feature: implementing client connect 2024-04-13 16:21:34 +02:00
tsukino
b3009023b3 append message to list 2024-04-13 16:14:09 +02:00
tsukino
02398eb253 wip: use pair id in chatbox 2024-04-13 16:12:31 +02:00
tsukino
f2e298943f wip: consolidate logic to ChatBox 2024-04-13 16:11:38 +02:00
tsukino
743b073fbe wip: add new rpc method to p2p 2024-04-13 16:06:37 +02:00
tsukino
78fc4bc452 wip: add chatbox 2024-04-13 15:53:51 +02:00
tsukino
c0fb13d5c8 wip: adding verifier ux 2024-04-13 14:16:51 +02:00
John Shaw
6ffe529144 refactor: p2p chatting 2024-04-13 13:40:10 +02:00
John Shaw
82d5b77969 feature: websocket connections + chatting 2024-04-13 13:29:31 +02:00
John Shaw
f58431cfb2 refactor: p2p component 2024-04-13 12:35:22 +02:00
John Shaw
86f21ac8a1 feature: creating the p2p ui component 2024-04-13 12:24:45 +02:00
tsukino
a42bb2eabd fix: clean up styling and code for sharing a proof (#51)
* fix: clean up styling and code for sharing a proof

* chore: update lockfiles

* fix: use constant for explorer api
2024-03-26 07:16:41 -04:00
tsukino
76c6acd998 chore: update lockfiles (#50) 2024-03-26 05:49:07 -04:00
Richard Liang
d0024077f9 add ext reloader to fix infinite refresh (#49) 2024-03-26 05:38:34 -04:00
Tanner
42ab67eb24 Merge pull request #47 from tlsnotary/upload-proof-to-ipfs
Upload proof to IPFS
2024-03-20 18:20:19 -07:00
Tanner Shaw
028a3b5444 chore: fixing linting errors 2024-03-20 18:18:44 -07:00
Tanner Shaw
d60d6a3ff3 refactor: modal events 2024-03-05 14:32:43 -08:00
Tanner Shaw
33ad9acca5 refactor: changing styling to modal 2024-03-05 13:34:59 -08:00
Tanner Shaw
6503281d75 refactor: adding styling to modal 2024-03-04 13:28:41 -08:00
Tanner Shaw
5f7bc6dae0 refactor: trying to fix some of the GH action checks 2024-03-03 20:30:33 -08:00
Tanner Shaw
bc4e77b8f1 feature: added modal for user acceptance to upload proof to IPFS 2024-03-03 16:09:32 -08:00
Tanner Shaw
8ec3e37c92 feature: added button to share proofs through ipfs 2024-03-03 14:15:57 -08:00
tsukino
220c138290 chore: update tlsn-js (#46) 2024-02-22 11:33:23 -05:00
Hendrik Eeckhaut
eb505cf234 Merge pull request #45 from tlsnotary/notary-server-documentation
Update readme for version constraint
2024-02-20 14:33:05 +01:00
Christopher Chong
04485168e1 Update readme for notary version. 2024-02-19 20:29:20 +08:00
tsukino
fec058fd7c feat: upgrade to alpha.4 (#43) 2024-02-16 11:46:21 -05:00
tsukino
042fba9c09 Update README.md 2024-02-15 04:32:54 -05:00
tsukino
1da4f45564 feat: add error handling to prover (#42) 2024-02-06 09:09:56 -05:00
Hendrik Eeckhaut
c8f2b541d6 build: added cocogitto config file (#40) 2024-01-09 10:22:38 -05:00
25 changed files with 19541 additions and 9164 deletions

1
.gitignore vendored
View File

@@ -8,3 +8,4 @@ bin/
build
tlsn/
zip
yarn.lock

View File

@@ -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
View 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

File diff suppressed because it is too large Load Diff

View File

@@ -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

File diff suppressed because it is too large Load Diff

View 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>
);
}

View File

@@ -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,
)}
>

View File

@@ -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: {

View File

@@ -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: {

View File

@@ -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>

View 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>
);
}

View 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 />;
}

View File

@@ -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>
);
}

View File

@@ -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">

View File

@@ -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
View 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>
);
}

View File

@@ -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';

View File

@@ -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
View 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
View File

@@ -0,0 +1 @@
export const EXPLORER_API = 'http://localhost:3000';

View File

@@ -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;
}
}

View File

@@ -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,
};

View File

@@ -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: [
{

10987
yarn.lock

File diff suppressed because it is too large Load Diff