Compare commits

...

20 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
13 changed files with 5501 additions and 4986 deletions

1
.gitignore vendored
View File

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

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

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

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

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

View File

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

9970
yarn.lock

File diff suppressed because it is too large Load Diff