diff --git a/src/App.tsx b/src/App.tsx
index d47b976..30f3843 100644
--- a/src/App.tsx
+++ b/src/App.tsx
@@ -1,20 +1,32 @@
-import React from 'react';
+import React, { useEffect } from 'react';
import Welcome from './containers/Welcome';
import Connected from './containers/Connected';
-import useConnection, { ConnectionStates } from './hooks/useConnection';
+import { SessionProvider } from './contexts/SessionContext';
+import useSessions from './hooks/useSessions';
+import Session from './containers/Session';
const App: React.FC<{}> = () => {
- const { state } = useConnection();
+ const { sessions, addSession } = useSessions();
- if (state === ConnectionStates.WAITING) {
- return
- }
- if (state === ConnectionStates.CONNECTED) {
- return
+ useEffect(() => {
+ if (sessions.length === 0) {
+ addSession()
+ }
+ }, [sessions.length]);
+
+ if (sessions.length === 0) {
+ return
Setting up
}
+
return (
- Connected
- );
+ <>
+ {sessions.map((session) => (
+
+
+
+ ))}
+ >
+ )
};
export default App;
diff --git a/src/Crypto/index.tsx b/src/Crypto/index.tsx
new file mode 100644
index 0000000..09d848d
--- /dev/null
+++ b/src/Crypto/index.tsx
@@ -0,0 +1,30 @@
+import { encrypt, decrypt } from '../utils/crypto';
+
+class Crypto {
+ #secret: string;
+ #ticket?: Symbol;
+
+ constructor(secret: string, ticket?: Symbol) {
+ this.#secret = secret;
+ this.#ticket = ticket
+ }
+
+ encrypt = async (data: any) => {
+ const raw = JSON.stringify(data);
+ const result = await encrypt(raw, this.#secret);
+ return result;
+ };
+
+ decrypt = async (data: any) => {
+ return decrypt(data, this.#secret);
+ };
+
+ getSecret(ticket: Symbol) {
+ if (!this.#ticket || this.#ticket !== ticket) {
+ throw new Error('Ticket not valid');
+ }
+ return this.#secret;
+ }
+}
+
+export default Crypto;
diff --git a/src/hooks/useMessages.ts b/src/MessageList.ts
similarity index 66%
rename from src/hooks/useMessages.ts
rename to src/MessageList.ts
index afa84f5..4596cfa 100644
--- a/src/hooks/useMessages.ts
+++ b/src/MessageList.ts
@@ -1,5 +1,5 @@
+import EventEmitter from 'eventemitter3';
import { nanoid } from 'nanoid';
-import { useState, useCallback } from 'react';
interface BaseRequest {
type: string;
@@ -82,40 +82,48 @@ const updateMessage = (
};
};
-const useMessages = (postProcess: (input: any) => any) => {
- const [messages, setMessage] = useState([]);
+class MessageList extends EventEmitter {
+ #messages: Message[] = [];
+ #postProcess: (a: any) => any;
- const addMessage = useCallback((request: Request, self: boolean) => {
- setMessage((current) => {
- if (request.type === 'start-message') {
- const message: IncompleteMessage = {
- id: request.payload.id,
- type: 'incomplete',
- self,
- length: request.payload.length,
- current: 0,
- parts: [],
- };
- return [
- ...current,
- message,
- ];
- }
+ constructor(postProces: (a: any) => any = (a) => a) {
+ super();
+ this.#postProcess = postProces;
+ }
- if (request.type === 'update-message') {
- return current.map(message => {
- if (message.id !== request.payload.id) {
- return message;
- }
- return updateMessage(message, request, postProcess);
- });
- }
+ get list() {
+ return this.#messages;
+ }
- return current;
- });
- }, []);
+ addMessage = (request: Request, self: boolean) => {
+ if (request.type === 'start-message') {
+ const message: IncompleteMessage = {
+ id: request.payload.id,
+ type: 'incomplete',
+ self,
+ length: request.payload.length,
+ current: 0,
+ parts: [],
+ };
+ this.#messages = [
+ ...this.#messages,
+ message,
+ ];
+ this.emit('updated');
+ }
- const formatMessage = (msg: any) => {
+ if (request.type === 'update-message') {
+ this.#messages = this.#messages.map(message => {
+ if (message.id !== request.payload.id) {
+ return message;
+ }
+ return updateMessage(message, request, this.#postProcess);
+ });
+ this.emit('updated');
+ }
+ };
+
+ formatMessage = (msg: any) => {
const dataString = JSON.stringify(msg);
const parts = chunkSubstr(dataString, 100000);
const id = nanoid();
@@ -139,12 +147,6 @@ const useMessages = (postProcess: (input: any) => any) => {
updateMsgs,
};
};
-
- return {
- messages,
- addMessage,
- formatMessage,
- }
};
-export default useMessages;
+export default MessageList;
diff --git a/src/Session/index.tsx b/src/Session/index.tsx
new file mode 100644
index 0000000..41a2aa1
--- /dev/null
+++ b/src/Session/index.tsx
@@ -0,0 +1,121 @@
+import Peer, { DataConnection } from 'peerjs';
+import Crypto from '../Crypto';
+import MessageList from '../MessageList';
+import EventEmitter from 'eventemitter3';
+import { nanoid } from 'nanoid';
+
+enum State {
+ READY,
+ CONNECTING,
+ CONNECTED,
+ DISCONNECTED,
+};
+
+class Session extends EventEmitter {
+ #cryptoTicket = Symbol('crypto-ticket');
+ #name: string;
+ #peer: Peer;
+ #connection?: DataConnection;
+ #crypto: Crypto;
+ #messages: MessageList = new MessageList();
+ #state: State = State.READY;
+
+ constructor(
+ name: string = 'unnamed',
+ id: string = nanoid(),
+ secret: string = nanoid(),
+ ) {
+ super();
+ this.#name = name;
+ this.#peer = new Peer(id);
+ this.#crypto = new Crypto(secret, this.#cryptoTicket);
+ this.#peer.on('connection', this.#handleConnection);
+ this.#messages.on('updated', () => {
+ this.emit('updated');
+ });
+ }
+
+ #handleConnection = (connection: DataConnection) => {
+ if (this.#connection) {
+ return;
+ }
+ this.#connection = connection;
+ this.#connection.on('data', this.#handleData);
+ this.#connection.on('close', this.#handleDisconnect);
+ this.#connection.on('error', this.#handleDisconnect);
+ this.#state = State.CONNECTED;
+ this.emit('updated');
+ }
+
+ #handleData = async (encrypted: any) => {
+ const message = await this.#crypto.decrypt(encrypted);
+ this.#messages.addMessage(message, false);
+ console.log('foo', message);
+ }
+
+ #reconnect = () => {
+ if (!this.#connection) return;
+ const id = this.#connection.peer;
+ const secret = this.#crypto.getSecret(this.#cryptoTicket);
+ // TODO: Add reconnect functionality
+ }
+
+ #handleDisconnect = () => {
+ this.#state = State.DISCONNECTED;
+ this.emit('updated');
+ }
+
+ get id() {
+ return this.#peer.id;
+ }
+
+ get name() {
+ return this.#name;
+ }
+
+ get messages() {
+ return this.#messages.list;
+ }
+
+ get state() {
+ return this.#state;
+ }
+
+ get connectInfo() {
+ return {
+ id: this.#peer.id,
+ secret: this.#crypto.getSecret(this.#cryptoTicket),
+ }
+ }
+
+ connect = (id: string, secret: string) => {
+ this.#state = State.CONNECTING;
+ this.#crypto = new Crypto(secret, this.#cryptoTicket);
+ this.#connection = this.#peer.connect(id);
+ this.#connection.on('close', this.#handleDisconnect);
+ this.#connection.on('error', this.#handleDisconnect);
+ this.emit('updated');
+ this.#connection.on('open', () => {
+ this.#state = State.CONNECTED;
+ this.emit('updated');
+ });
+ };
+
+ send = async (data: any) => {
+ if (!this.#connection) {
+ throw new Error('Not connected');
+ }
+ const { startMsg, updateMsgs } = this.#messages.formatMessage(data);
+
+ this.#messages.addMessage(startMsg, true);
+ this.#connection.send(await this.#crypto.encrypt(startMsg));
+ for (let updateMsg of updateMsgs) {
+ this.#connection.send(await this.#crypto.encrypt(updateMsg));
+ this.#messages.addMessage(updateMsg, true);
+ }
+ };
+};
+
+export { State };
+
+export default Session;
diff --git a/src/containers/Connected.tsx b/src/containers/Connected.tsx
index e4db71a..c763e53 100644
--- a/src/containers/Connected.tsx
+++ b/src/containers/Connected.tsx
@@ -1,6 +1,6 @@
import React, { useCallback, useMemo, useState } from 'react';
import styled from 'styled-components';
-import useConnection from '../hooks/useConnection';
+import useSession, { State } from '../hooks/useSession';
import Message from '../components/Message';
import ComposeMessage from '../types/ComposeMessage';
import ComposeBar from '../components/ComposeBar';
@@ -22,7 +22,7 @@ const Loading = styled.div`
const Connected: React.FC<{}> = () => {
- const { send, messages } = useConnection();
+ const { send, messages, state } = useSession();
const [currentMessage, setCurrentMessage] = useState({
files: [],
text: '',
@@ -48,7 +48,9 @@ const Connected: React.FC<{}> = () => {
Loading {Math.round(message.current / message.length * 100)}%
)))}
-
+ { state === State.CONNECTED && (
+
+ )}
);
}
diff --git a/src/containers/Session.tsx b/src/containers/Session.tsx
new file mode 100644
index 0000000..21fc755
--- /dev/null
+++ b/src/containers/Session.tsx
@@ -0,0 +1,24 @@
+import React from 'react';
+import useSession, { State } from '../hooks/useSession';
+import Welcome from './Welcome';
+import Connected from './Connected';
+
+const Session: React.FC<{}> = () => {
+ const { state } = useSession();
+
+ if (state === State.READY) {
+ return (
+
+ );
+ }
+
+ if (state === State.CONNECTED) {
+ return
+ }
+
+ return (
+ {state.toString()}
+ )
+};
+
+export default Session;
diff --git a/src/containers/Welcome.tsx b/src/containers/Welcome.tsx
index 83d4fc5..6a1c12d 100644
--- a/src/containers/Welcome.tsx
+++ b/src/containers/Welcome.tsx
@@ -1,8 +1,8 @@
-import React, { useState, useCallback, useEffect, useMemo, useRef } from 'react';
+import React, { useState, useCallback } from 'react';
import styled from 'styled-components';
import QRCode from 'react-qr-code';
import QRReader from 'react-qr-reader'
-import useConnection from '../hooks/useConnection';
+import useSession from '../hooks/useSession';
const Wrapper = styled.div`
display: flex;
@@ -32,14 +32,15 @@ const Content = styled.div`
`;
const Welcome: React.FC<{}> = () => {
- const { connect, clientInfo } = useConnection();
+ const { connect, connectInfo } = useSession();
const [mode, setMode] = useState<'view' | 'scan'>('view');
const onScan = useCallback(
(result) => {
if (result) {
setMode('view');
- connect(JSON.parse(result));
+ const { id, secret } = JSON.parse(result);
+ connect(id, secret);
}
},
[],
@@ -52,9 +53,10 @@ const Welcome: React.FC<{}> = () => {
+ {JSON.stringify(connectInfo)}
{mode === 'view' && (
)}
diff --git a/src/contexts/ConnectionContext.tsx b/src/contexts/ConnectionContext.tsx
deleted file mode 100644
index 406af6c..0000000
--- a/src/contexts/ConnectionContext.tsx
+++ /dev/null
@@ -1,127 +0,0 @@
-import React, { createContext, useMemo, useState, useCallback, useEffect } from 'react';
-import Peer, { DataConnection } from 'peerjs';
-import { nanoid } from 'nanoid';
-import useCrypto from '../hooks/useCrypto';
-import useMessages from '../hooks/useMessages';
-
-enum States {
- WAITING,
- CONNECTING,
- CONNECTED,
-}
-
-interface ConnectionContextValue {
- clientInfo: any;
- state: States;
- messages: any[];
- send: (message: any) => Promise;
- connect: (connectionInfo: any) => Promise;
-}
-
-function dataURItoBlob(dataURI: string) {
- var mime = dataURI.split(',')[0].split(':')[1].split(';')[0];
- var binary = atob(dataURI.split(',')[1]);
- var array = [];
- for (var i = 0; i < binary.length; i++) {
- array.push(binary.charCodeAt(i));
- }
- const blob = new Blob([new Uint8Array(array)], {type: mime});
- return URL.createObjectURL(blob);
-}
-
-const ConnectionContext = createContext(undefined as any);
-
-const postProcess = (input: any) => {
- if (input.mediaType === 'file') {
- return {
- ...input,
- body: dataURItoBlob(input.body),
- };
- }
- return input;
-};
-
-const ConnectionProvider: React.FC = ({ children }) => {
-
- const { messages, addMessage, formatMessage } = useMessages(postProcess);
- const [secret, setSecret] = useState(nanoid());
- const { encrypt, decrypt } = useCrypto(secret);
- const id = useMemo(() => nanoid(), []);
- const peer = useMemo(() => new Peer(id), [id]);
- const [connection, setConnection] = useState(undefined);
- const [state, setState] = useState(States.WAITING);
- const clientInfo = useMemo(() => ({
- id,
- secret,
- }), [id]);
-
- const send = useCallback(async (message: any) => {
- if (!connection) return;
- const { startMsg, updateMsgs } = formatMessage(message);
-
- addMessage(startMsg, true);
- connection.send(await encrypt(startMsg));
- for (let updateMsg of updateMsgs) {
- connection.send(await encrypt(updateMsg));
- addMessage(updateMsg);
- }
- }, [connection, encrypt]);
-
- const connect = useCallback(async (clientInfo: any) => {
- setState(States.CONNECTING);
- const newConnection = peer.connect(clientInfo.id);
- newConnection.on('open', () => {
- setSecret(clientInfo.secret);
- setState(States.CONNECTED);
- setConnection(newConnection);
- });
- }, [peer]);
-
- useEffect(() => {
- if (connection) {
- return;
- }
- const onConnect = (newConnection: DataConnection) => {
- setState(States.CONNECTED);
- setConnection(newConnection);
- };
- peer.on('connection', onConnect);
-
- return () => {
- peer.off('connection', onConnect);
- };
- }, [peer, connection]);
-
- useEffect(() => {
- if (!connection) {
- return;
- }
- const handleData = async (encrypted: any) => {
- const message = await decrypt(encrypted);
- addMessage(message, false);
- };
- connection.on('data', handleData);
- return () => {
- connection.off('data', handleData);
- }
- }, [connection, decrypt]);
-
-
- return (
-
- {children}
-
- );
-};
-
-export { States, ConnectionProvider };
-
-export default ConnectionContext;
diff --git a/src/contexts/SessionContext.tsx b/src/contexts/SessionContext.tsx
new file mode 100644
index 0000000..fb3f9ce
--- /dev/null
+++ b/src/contexts/SessionContext.tsx
@@ -0,0 +1,54 @@
+import React, { createContext, ReactNode, useState, useEffect, useCallback } from 'react';
+import Session, { State } from '../Session';
+
+interface SessionContextValue {
+ messages: any[];
+ send: (data: any) => Promise;
+ connectInfo: any;
+ state: State;
+ connect: (id: string, secret: string) => any;
+}
+
+interface SessionProviderProps {
+ session: Session;
+ children: ReactNode;
+}
+
+type Message = any;
+
+const SessionContext = createContext(undefined as any);
+
+const SessionProvider: React.FC = ({ session, children }) => {
+ const [messages, setMessages] = useState(session.messages);
+ const [state, setState] = useState(session.state);
+ const update = useCallback(() => {
+ setMessages(session.messages);
+ setState(session.state);
+ }, [session]);
+
+ useEffect(() => {
+ session.on('updated', update);
+
+ return () => {
+ session.off('updated', update);
+ }
+ }, [session, update]);
+
+ return (
+
+ {children}
+
+ )
+};
+
+export { SessionProvider };
+
+export default SessionContext;
diff --git a/src/contexts/SessionsContext.tsx b/src/contexts/SessionsContext.tsx
new file mode 100644
index 0000000..5ef497c
--- /dev/null
+++ b/src/contexts/SessionsContext.tsx
@@ -0,0 +1,42 @@
+import { nanoid } from 'nanoid';
+import React, { createContext, useState, useCallback } from 'react';
+import Session from '../Session';
+
+interface ConnectionInfo {
+ id: string;
+ secret: string;
+}
+
+interface SessionsContextValue {
+ sessions: Session[];
+ addSession: (name?: string, id?: string, secret?: string) => void;
+}
+
+const SessionsContext = createContext(undefined as any);
+
+const SessionsProvider: React.FC = ({ children }) => {
+ const [sessions, setSessions] = useState([]);
+
+ const addSession = useCallback(() => {
+ const session = new Session('Unnamed session', nanoid(), nanoid());
+ setSessions(current => [
+ ...current,
+ session,
+ ]);
+ }, []);
+
+ return (
+
+ {children}
+
+ );
+};
+
+export { ConnectionInfo, SessionsProvider };
+
+export default SessionsContext;
diff --git a/src/hooks/useConnection.ts b/src/hooks/useConnection.ts
deleted file mode 100644
index 0b600d6..0000000
--- a/src/hooks/useConnection.ts
+++ /dev/null
@@ -1,11 +0,0 @@
-import { useContext } from 'react';
-import ConnectionContext, { States } from '../contexts/ConnectionContext';
-
-const useConnection = () => {
- const context = useContext(ConnectionContext);
- return context;
-};
-
-export const ConnectionStates = States;
-
-export default useConnection;
diff --git a/src/hooks/useCrypto.ts b/src/hooks/useCrypto.ts
deleted file mode 100644
index 8656da8..0000000
--- a/src/hooks/useCrypto.ts
+++ /dev/null
@@ -1,22 +0,0 @@
-import { useCallback } from 'react';
-import { encrypt, decrypt } from '../utils/crypto';
-
-
-const useCrypto = (secret: string) => {
- const doEncrypt = useCallback(async (data: any) => {
- const raw = JSON.stringify(data);
- const result = await encrypt(raw, secret);
- return result;
- }, [secret]);
-
- const doDecrypt = useCallback(async (data: string[]) => {
- return decrypt(data, secret);
- }, [secret]);
-
- return {
- encrypt: doEncrypt,
- decrypt: doDecrypt,
- };
-}
-
-export default useCrypto;
diff --git a/src/hooks/useSession.ts b/src/hooks/useSession.ts
new file mode 100644
index 0000000..9ce8b06
--- /dev/null
+++ b/src/hooks/useSession.ts
@@ -0,0 +1,12 @@
+import { useContext } from 'react';
+import { State } from '../Session';
+import SessionContext from '../contexts/SessionContext';
+
+const useSession = () => {
+ const context = useContext(SessionContext);
+ return context;
+};
+
+export { State };
+
+export default useSession;
diff --git a/src/hooks/useSessions.ts b/src/hooks/useSessions.ts
new file mode 100644
index 0000000..3805eab
--- /dev/null
+++ b/src/hooks/useSessions.ts
@@ -0,0 +1,9 @@
+import { useContext } from 'react';
+import SessionsContext from '../contexts/SessionsContext';
+
+const useSessions = () => {
+ const context = useContext(SessionsContext);
+ return context;
+};
+
+export default useSessions;
diff --git a/src/index.tsx b/src/index.tsx
index 0358b67..bf6c491 100644
--- a/src/index.tsx
+++ b/src/index.tsx
@@ -1,13 +1,13 @@
import React from 'react';
import { render } from 'react-dom';
-import { ConnectionProvider } from './contexts/ConnectionContext';
+import { SessionsProvider } from './contexts/SessionsContext';
import App from './App';
const root = document.getElementById('root');
const app = (
-
+
-
+
);
render(app, root);