diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..2381589 --- /dev/null +++ b/client/README.md @@ -0,0 +1,102 @@ +# Chat E2EE Client + +React-based client application for Chat E2EE - a secure, end-to-end encrypted messaging platform. + +## Tech Stack + +- **React 18** - Modern React with hooks +- **TypeScript** - Type-safe development +- **Vite** - Fast build tool and dev server +- **@chat-e2ee/service** - SDK for backend communication and encryption + +## Project Structure + +``` +client/ +├── src/ +│ ├── components/ # React components +│ │ ├── SetupOverlay.tsx # Channel creation/join UI +│ │ ├── ChatContainer.tsx # Main chat interface +│ │ ├── Message.tsx # Individual message display +│ │ └── CallOverlay.tsx # Audio call interface +│ ├── App.tsx # Main application component +│ ├── main.tsx # Application entry point +│ └── style.css # Global styles +├── index.html # HTML template +├── vite.config.ts # Vite configuration +├── tsconfig.json # TypeScript configuration +└── package.json # Dependencies and scripts +``` + +## Development + +### Prerequisites + +- Node.js 16 or higher +- npm + +### Setup + +Install dependencies: +```bash +npm install +``` + +### Running the Dev Server + +```bash +npm run dev +``` + +The client will start on `http://localhost:3000` + +### Building for Production + +```bash +npm run build +``` + +The production build will be output to the `build/` directory. + +### Running the Preview Server + +```bash +npm run preview +``` + +## Features + +- **End-to-End Encryption**: All messages are encrypted using RSA/AES encryption +- **Audio Calls**: WebRTC-based encrypted audio calling +- **Channel System**: Create or join channels using unique hashes +- **No Registration**: No user accounts or personal data required +- **Modern UI**: Clean, responsive design with glassmorphism effects + +## How It Works + +1. **Create Channel**: Generate a unique channel hash +2. **Share Hash**: Share the hash with the person you want to chat with +3. **Connect**: Both users connect to the same channel +4. **Chat Securely**: All messages are encrypted end-to-end + +## Security + +- Private keys are generated locally and never leave the device +- Messages are encrypted with recipient's public key +- Audio streams use insertable streams API for encryption +- No chat history is stored on the server + +## Architecture + +The client uses React for UI rendering and state management. Communication with the backend is handled through the `@chat-e2ee/service` SDK, which provides: + +- Socket.io for real-time communication +- WebRTC for peer-to-peer audio calls +- Crypto utilities for encryption/decryption +- Channel and user management + +## Notes + +- The app requires a running backend server (configured via `CHATE2EE_API_URL`) +- By default, it connects to the production backend at `chat-e2ee-2.azurewebsites.net` +- For local development, ensure the backend server is running on the expected port diff --git a/client/app.ts b/client/app.ts deleted file mode 100644 index 92b1176..0000000 --- a/client/app.ts +++ /dev/null @@ -1,303 +0,0 @@ -import { createChatInstance, utils } from '@chat-e2ee/service'; - -// State -let chat: any = null; -let userId: string = ''; -let channelHash: string = ''; -let privateKey: string = ''; - -// DOM Elements -// DOM Elements -const setupOverlay = document.getElementById('setup-overlay')!; -const initialActions = document.getElementById('initial-actions')!; -const createHashView = document.getElementById('create-hash-view')!; -const joinHashView = document.getElementById('join-hash-view')!; -const finalActions = document.getElementById('final-actions')!; - -const showCreateBtn = document.getElementById('show-create-hash') as HTMLButtonElement; -const showJoinBtn = document.getElementById('show-join-hash') as HTMLButtonElement; -const backBtn = document.getElementById('back-btn') as HTMLButtonElement; -const copyHashBtn = document.getElementById('copy-hash-btn') as HTMLButtonElement; - -const generatedHashDisplay = document.getElementById('generated-hash-display') as HTMLInputElement; -const hashInput = document.getElementById('channel-hash') as HTMLInputElement; -const joinBtn = document.getElementById('join-btn') as HTMLButtonElement; -const setupStatus = document.getElementById('setup-status')!; - -const chatContainer = document.getElementById('chat-container')!; -const messagesArea = document.getElementById('messages-area')!; -const msgInput = document.getElementById('msg-input') as HTMLInputElement; -const sendBtn = document.getElementById('send-btn') as HTMLButtonElement; -const startCallBtn = document.getElementById('start-call-btn') as HTMLButtonElement; -const chatHeader = document.querySelector('header')!; -const participantInfo = document.getElementById('participant-info')!; -const headerHashDisplay = document.getElementById('channel-hash-display')!; -const headerHashText = document.getElementById('header-hash')!; -const copyHeaderHashBtn = document.getElementById('copy-header-hash') as HTMLButtonElement; - -// Call Elements -const callOverlay = document.getElementById('call-overlay')!; -const callStatusText = document.getElementById('call-status')!; -const endCallBtn = document.getElementById('end-call-btn') as HTMLButtonElement; -const callDuration = document.getElementById('call-duration')!; - -// Initialize Chat -async function initChat() { - try { - setupStatus.textContent = 'Initializing secure keys...'; - chat = createChatInstance(); - await chat.init(); - - const keys = chat.getKeyPair(); - privateKey = keys.privateKey; - setupStatus.textContent = ''; - - // Check for URL hash on load - handleUrlHash(); - } catch (err) { - console.error('Init error:', err); - setupStatus.textContent = 'Initialization failed. Refresh and try again.'; - } -} - -// UI Navigation -function showView(view: 'initial' | 'create' | 'join') { - initialActions.classList.add('hidden'); - createHashView.classList.add('hidden'); - joinHashView.classList.add('hidden'); - finalActions.classList.add('hidden'); - setupStatus.textContent = ''; - - if (view === 'initial') { - initialActions.classList.remove('hidden'); - } else if (view === 'create') { - createHashView.classList.remove('hidden'); - finalActions.classList.remove('hidden'); - } else if (view === 'join') { - joinHashView.classList.remove('hidden'); - finalActions.classList.remove('hidden'); - hashInput.focus(); - } -} - -showCreateBtn.addEventListener('click', async () => { - showView('create'); - try { - generatedHashDisplay.value = 'Generating...'; - const linkObj = await chat.getLink(); - generatedHashDisplay.value = linkObj.hash; - channelHash = linkObj.hash; - } catch (err) { - setupStatus.textContent = 'Failed to generate hash.'; - } -}); - -showJoinBtn.addEventListener('click', () => { - showView('join'); -}); - -backBtn.addEventListener('click', () => { - showView('initial'); - channelHash = ''; - hashInput.value = ''; -}); - -copyHashBtn.addEventListener('click', () => { - navigator.clipboard.writeText(generatedHashDisplay.value); - const originalText = setupStatus.textContent; - setupStatus.textContent = 'Hash copied to clipboard!'; - setTimeout(() => setupStatus.textContent = originalText, 2000); -}); - -copyHeaderHashBtn.addEventListener('click', () => { - navigator.clipboard.writeText(headerHashText.textContent || ''); - const originalText = setupStatus.textContent; - setupStatus.textContent = 'Hash copied to clipboard!'; - setTimeout(() => setupStatus.textContent = originalText, 2000); -}); - -async function checkExistingUsers() { - try { - const users = await chat.getUsersInChannel(); - if (users && users.length > 1) { - chatHeader.classList.add('active'); - participantInfo.textContent = 'Peer is already here. Communication is encrypted.'; - } - } catch (err) { - console.error('Error checking users:', err); - } -} - -function updateUrlHash(hash: string) { - if (hash) { - window.location.hash = hash; - } -} - -function handleUrlHash() { - const hash = window.location.hash.replace('#', ''); - if (hash && hash.length > 5) { - hashInput.value = hash; - showView('join'); - } -} - -joinBtn.addEventListener('click', async () => { - // Determine which hash to use - const enteredHash = hashInput.value.trim(); - const finalHash = enteredHash || channelHash; - - if (!finalHash) { - setupStatus.textContent = 'Please enter or generate a hash.'; - return; - } - - // Auto-generate User ID - if (!userId) { - userId = (utils as any).generateUUID(); - } - - try { - joinBtn.disabled = true; - setupStatus.textContent = 'Connecting...'; - await chat.setChannel(finalHash, userId); - - // Update UI with Hash - headerHashText.textContent = finalHash; - headerHashDisplay.classList.remove('hidden'); - updateUrlHash(finalHash); - - setupOverlay.classList.add('hidden'); - chatContainer.classList.remove('hidden'); - - setupChatListeners(); - await checkExistingUsers(); - } catch (err) { - console.error('Join error:', err); - setupStatus.textContent = 'Failed to connect.'; - joinBtn.disabled = false; - } -}); - -function setupChatListeners() { - chat.on('on-alice-join', () => { - chatHeader.classList.add('active'); - participantInfo.textContent = 'Peer joined. Communication is encrypted.'; - }); - - chat.on('on-alice-disconnect', () => { - chatHeader.classList.remove('active'); - participantInfo.textContent = 'Peer disconnected.'; - }); - - chat.on('chat-message', async (msg: any) => { - const plainText = await (utils as any).decryptMessage(msg.message, privateKey); - appendMessage(msg.sender, plainText, 'received'); - }); - - chat.on('call-added', (call: any) => { - showCallOverlay('Incoming Call...'); - setupCallListeners(call); - }); -} - -// Messaging -async function sendMessage() { - const text = msgInput.value.trim(); - if (!text) return; - - msgInput.value = ''; - appendMessage(userId, text, 'sent'); - - try { - await chat.encrypt({ text }).send(); - } catch (err) { - console.error('Send error:', err); - } -} - -sendBtn.addEventListener('click', sendMessage); -msgInput.addEventListener('keypress', (e) => { - if (e.key === 'Enter') sendMessage(); -}); - -function appendMessage(sender: string, text: string, type: 'sent' | 'received') { - const msgEl = document.createElement('div'); - msgEl.className = `message ${type}`; - - const time = new Date().toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); - - msgEl.innerHTML = ` -
${text}
-
- ${sender} - ${time} -
- `; - - messagesArea.appendChild(msgEl); - messagesArea.scrollTop = messagesArea.scrollHeight; -} - -// Calling -let callTimer: any = null; -let callStartTime: number = 0; - -startCallBtn.addEventListener('click', async () => { - try { - const call = await chat.startCall(); - showCallOverlay('Calling...'); - setupCallListeners(call); - } catch (err: any) { - alert(err.message); - } -}); - -function setupCallListeners(call: any) { - call.on('state-changed', (state: string) => { - callStatusText.textContent = state.charAt(0).toUpperCase() + state.slice(1); - - if (state === 'connected') { - startTimer(); - } - - if (state === 'closed' || state === 'failed') { - hideCallOverlay(); - stopTimer(); - } - }); - - endCallBtn.onclick = async () => { - await call.endCall(); - hideCallOverlay(); - stopTimer(); - }; -} - -function startTimer() { - stopTimer(); - callStartTime = Date.now(); - callTimer = setInterval(() => { - const seconds = Math.floor((Date.now() - callStartTime) / 1000); - const m = Math.floor(seconds / 60).toString().padStart(2, '0'); - const s = (seconds % 60).toString().padStart(2, '0'); - callDuration.textContent = `${m}:${s}`; - }, 1000); -} - -function stopTimer() { - if (callTimer) clearInterval(callTimer); - callDuration.textContent = '00:00'; -} - -function showCallOverlay(status: string) { - callOverlay.classList.remove('hidden'); - callStatusText.textContent = status; -} - -function hideCallOverlay() { - callOverlay.classList.add('hidden'); -} - -// Start -initChat(); diff --git a/client/index.html b/client/index.html index 191ff71..dd102d5 100644 --- a/client/index.html +++ b/client/index.html @@ -5,128 +5,14 @@ Chat E2EE - Minimal & Secure - -
- -
-
-

Secure Messenger

-

Simple. End-to-End Encrypted. Private.

- -
- - -
- - - - - - - -
-
-
- - - - - - -
- +
+ \ No newline at end of file diff --git a/client/package-lock.json b/client/package-lock.json index 20f112f..74b4451 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -8,9 +8,14 @@ "name": "chat-e2ee-client", "version": "1.0.0", "dependencies": { - "@chat-e2ee/service": "file:../service" + "@chat-e2ee/service": "file:../service", + "react": "^19.2.3", + "react-dom": "^19.2.3" }, "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", "typescript": "^5.6.2", "vite": "^5.4.1" } @@ -30,6 +35,288 @@ "typescript": "^5.9.3" } }, + "node_modules/@babel/code-frame": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", + "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.27.1", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.5.tgz", + "integrity": "sha512-6uFXyCayocRbqhZOB+6XcuZbkMNimwfVGFji8CTZnCzOHVGvDqzvitu1re2AU5LROliz7eQPhB8CpAMvnx9EjA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.5.tgz", + "integrity": "sha512-e7jT4DxYvIDLk1ZHmU/m/mB19rex9sv0c2ftBtjSBv+kVM/902eh0fINUzD7UwLLNR+jU585GxUJ8/EBfAM5fw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.28.3", + "@babel/helpers": "^7.28.4", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.5.tgz", + "integrity": "sha512-3EwLFhZ38J4VyIP6WNtt2kUdW9dokXA9Cr4IVIFHuCpZ3H8/YFOl5JjZHisrn1fATPBmKKqXzDFvh9fUwHz6CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.28.5", + "@babel/types": "^7.28.5", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", + "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.27.2", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", + "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.27.1", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", + "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.27.1", + "@babel/helper-validator-identifier": "^7.27.1", + "@babel/traverse": "^7.28.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", + "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.4", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", + "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.4" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.5.tgz", + "integrity": "sha512-KKBU1VGYR7ORr3At5HAtUQ+TV3SzRCXmA/8OdDZiLDBIZxVyzXuztPjfLd3BV1PRAQGCMWWSHYhL0F8d5uHBDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.5" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-self": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-self/-/plugin-transform-react-jsx-self-7.27.1.tgz", + "integrity": "sha512-6UzkCs+ejGdZ5mFFC/OCUrv028ab2fp1znZmCZjAOBKiBK2jXD1O+BPSfX8X2qjJ75fZBMSnQn3Rq2mrBJK2mw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-source": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-source/-/plugin-transform-react-jsx-source-7.27.1.tgz", + "integrity": "sha512-zbwoTsBruTeKB9hSq73ha66iFeJHuaFkUbwvqElnygoNbj/jHRsSeokowZFN3CZ64IvEqcmmkVe89OPXc7ldAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.27.2", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", + "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/parser": "^7.27.2", + "@babel/types": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.5.tgz", + "integrity": "sha512-TCCj4t55U90khlYkVV/0TfkJkAkUg3jZFA3Neb7unZT8CPok7iiRfaX0F+WnqWqt7OxhOn0uBKXCw4lbL8W0aQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.5", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.28.5", + "@babel/template": "^7.27.2", + "@babel/types": "^7.28.5", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.5.tgz", + "integrity": "sha512-qQ5m48eI/MFLQ5PxQj4PFaprjyCTLI37ElWMmNs0K8Lk3dVeOdNpB3ks8jc7yM5CDmVC73eMVk/trk3fgmrUpA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@chat-e2ee/service": { "resolved": "../service", "link": true @@ -425,6 +712,63 @@ "node": ">=12" } }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.53", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.53.tgz", + "integrity": "sha512-vENRlFU4YbrwVqNDZ7fLvy+JR1CRkyr01jhSiDpE1u6py3OMzQfztQU2jxykW3ALNxO4kSlqIDeYyD0Y9RcQeQ==", + "dev": true, + "license": "MIT" + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.1.tgz", @@ -775,6 +1119,51 @@ "win32" ] }, + "node_modules/@types/babel__core": { + "version": "7.20.5", + "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", + "integrity": "sha512-qoQprZvz5wQFJwMDqeseRXWv3rqMvhgpbXFfVyWhbx9X47POIA6i/+dXefEmZKoAgOaTdaIgNSMqMIU61yRyzA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.20.7", + "@babel/types": "^7.20.7", + "@types/babel__generator": "*", + "@types/babel__template": "*", + "@types/babel__traverse": "*" + } + }, + "node_modules/@types/babel__generator": { + "version": "7.27.0", + "resolved": "https://registry.npmjs.org/@types/babel__generator/-/babel__generator-7.27.0.tgz", + "integrity": "sha512-ufFd2Xi92OAVPYsy+P4n7/U7e68fex0+Ee8gSG9KX7eo084CWiQ4sdxktvdl0bOPupXtVJPY19zk6EwWqUQ8lg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__template": { + "version": "7.4.4", + "resolved": "https://registry.npmjs.org/@types/babel__template/-/babel__template-7.4.4.tgz", + "integrity": "sha512-h/NUaSyG5EyxBIp8YRxo4RMe2/qQgvyowRwVMzhYhBCONbW8PUsg4lkFMrhgZhUe5z3L3MiLDuvyJ/CaPa2A8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.1.0", + "@babel/types": "^7.0.0" + } + }, + "node_modules/@types/babel__traverse": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@types/babel__traverse/-/babel__traverse-7.28.0.tgz", + "integrity": "sha512-8PvcXf70gTDZBgt9ptxJ8elBeBjcLOAcOtoO/mPJjtji1+CdGbHgm77om1GrsPxsiE+uXIpNSK64UYaIwQXd4Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/types": "^7.28.2" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -782,6 +1171,151 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/react": { + "version": "19.2.7", + "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.7.tgz", + "integrity": "sha512-MWtvHrGZLFttgeEj28VXHxpmwYbor/ATPYbBfSFZEIRK0ecCFLl2Qo55z52Hss+UV9CRN7trSeq1zbgx7YDWWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==", + "dev": true, + "license": "MIT", + "peerDependencies": { + "@types/react": "^19.2.0" + } + }, + "node_modules/@vitejs/plugin-react": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.1.2.tgz", + "integrity": "sha512-EcA07pHJouywpzsoTUqNh5NwGayl2PPVEJKUSinGGSxFGYn+shYbqMGBg6FXDqgXum9Ou/ecb+411ssw8HImJQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/core": "^7.28.5", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.53", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.18.0" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.9.11", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.9.11.tgz", + "integrity": "sha512-Sg0xJUNDU1sJNGdfGWhVHX0kkZ+HWcvmVymJbj6NSgZZmW/8S9Y2HQ5euytnIgakgxN6papOAWiwDo1ctFDcoQ==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.js" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001762", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001762.tgz", + "integrity": "sha512-PxZwGNvH7Ak8WX5iXzoK1KPZttBXNPuaOvI2ZYU7NrlM+d9Ov+TUvlLOBNGzVXAntMSMMlJPd+jY6ovrVjSmUw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.267", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.267.tgz", + "integrity": "sha512-0Drusm6MVRXSOJpGbaSVgcQsuB4hEkMpHXaVstcPmhu5LIedxs1xNK/nIxmQIU/RPC0+1/o0AVZfBTkTNJOdUw==", + "dev": true, + "license": "ISC" + }, "node_modules/esbuild": { "version": "0.21.5", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.21.5.tgz", @@ -821,6 +1355,16 @@ "@esbuild/win32-x64": "0.21.5" } }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/fsevents": { "version": "2.3.3", "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", @@ -836,6 +1380,66 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, "node_modules/nanoid": { "version": "3.3.11", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", @@ -855,6 +1459,13 @@ "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" } }, + "node_modules/node-releases": { + "version": "2.0.27", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.27.tgz", + "integrity": "sha512-nmh3lCkYZ3grZvqcCH+fjmQ7X+H0OeZgP40OierEaAptX4XofMh5kwNbWh7lBduUzCcV/8kZ+NDLCwm2iorIlA==", + "dev": true, + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -891,6 +1502,37 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/react": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react/-/react-19.2.3.tgz", + "integrity": "sha512-Ku/hhYbVjOQnXDZFv2+RibmLFGwFdeeKHFcOTlrt7xplBnya5OGn/hIRDsqDiSUcfORsDC7MPxwork8jBwsIWA==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "19.2.3", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.3.tgz", + "integrity": "sha512-yELu4WmLPw5Mr/lmeEpox5rw3RETacE++JgHqQzd2dg+YbJuat3jH4ingc+WPZhxaoFzdv9y33G+F7Nl5O0GBg==", + "license": "MIT", + "dependencies": { + "scheduler": "^0.27.0" + }, + "peerDependencies": { + "react": "^19.2.3" + } + }, + "node_modules/react-refresh": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", + "integrity": "sha512-QgT5//D3jfjJb6Gsjxv0Slpj23ip+HtOpnNgnb2S5zU3CB26G/IDPGoy4RJB42wzFE46DRsstbW6tKHoKbhAxw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/rollup": { "version": "4.55.1", "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.1.tgz", @@ -936,6 +1578,22 @@ "fsevents": "~2.3.2" } }, + "node_modules/scheduler": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz", + "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", + "license": "MIT" + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -960,6 +1618,37 @@ "node": ">=14.17" } }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/vite": { "version": "5.4.21", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.21.tgz", @@ -1019,6 +1708,13 @@ "optional": true } } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true, + "license": "ISC" } } } diff --git a/client/package.json b/client/package.json index 32b9c88..e2fc8c1 100644 --- a/client/package.json +++ b/client/package.json @@ -10,10 +10,15 @@ "start": "vite" }, "devDependencies": { + "@types/react": "^19.2.7", + "@types/react-dom": "^19.2.3", + "@vitejs/plugin-react": "^5.1.2", "typescript": "^5.6.2", "vite": "^5.4.1" }, "dependencies": { - "@chat-e2ee/service": "file:../service" + "@chat-e2ee/service": "file:../service", + "react": "^19.2.3", + "react-dom": "^19.2.3" } -} \ No newline at end of file +} diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..09f9819 --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,225 @@ +/** + * Chat E2EE - React Client Application + * + * A modern React-based client for end-to-end encrypted chat. + * Uses @chat-e2ee/service SDK for backend communication and encryption. + * + * Key features: + * - End-to-end encrypted messaging using RSA and AES + * - Audio calling with WebRTC + * - Channel-based communication with shareable hashes + * - No user registration required + */ + +import { useState, useEffect, useCallback } from 'react'; +import { createChatInstance, utils } from '@chat-e2ee/service'; +import type { IChatE2EE, IE2ECall } from '@chat-e2ee/service'; +import SetupOverlay from './components/SetupOverlay'; +import ChatContainer from './components/ChatContainer'; +import CallOverlay from './components/CallOverlay'; +import './style.css'; + +interface Message { + sender: string; + text: string; + type: 'sent' | 'received'; + timestamp: Date; +} + +/** + * Main application component for Chat E2EE + * + * Manages the overall application state including: + * - Chat initialization and connection + * - Message handling and display + * - Audio call functionality + * - User interface state (setup vs chat view) + */ +function App() { + const [chat, setChat] = useState(null); + const [userId, setUserId] = useState(''); + const [channelHash, setChannelHash] = useState(''); + const [privateKey, setPrivateKey] = useState(''); + const [showSetup, setShowSetup] = useState(true); + const [messages, setMessages] = useState([]); + const [peerConnected, setPeerConnected] = useState(false); + const [callActive, setCallActive] = useState(false); + const [callStatus, setCallStatus] = useState(''); + const [currentCall, setCurrentCall] = useState(null); + const [setupError, setSetupError] = useState(''); + + // Initialize chat on mount + useEffect(() => { + const initChat = async () => { + try { + const chatInstance = createChatInstance(); + await chatInstance.init(); + const keys = chatInstance.getKeyPair(); + setPrivateKey(keys.privateKey); + setChat(chatInstance); + } catch (err) { + console.error('Init error:', err); + setSetupError('Initialization failed. Refresh and try again.'); + } + }; + initChat(); + }, []); + + // Setup chat listeners + useEffect(() => { + if (!chat) return; + + const handleAliceJoin = () => { + setPeerConnected(true); + }; + + const handleAliceDisconnect = () => { + setPeerConnected(false); + }; + + const handleChatMessage = async (msg: any) => { + try { + const plainText = await (utils as any).decryptMessage(msg.message, privateKey); + setMessages((prev) => [ + ...prev, + { + sender: msg.sender, + text: plainText, + type: 'received', + timestamp: new Date(), + }, + ]); + } catch (err) { + console.error('Message decrypt error:', err); + } + }; + + const handleCallAdded = (call: any) => { + setCurrentCall(call); + setCallActive(true); + setCallStatus('Incoming Call...'); + setupCallListeners(call); + }; + + chat.on('on-alice-join', handleAliceJoin); + chat.on('on-alice-disconnect', handleAliceDisconnect); + chat.on('chat-message', handleChatMessage); + chat.on('call-added', handleCallAdded); + + // Note: The chat SDK doesn't support removing listeners + }, [chat, privateKey]); + + const setupCallListeners = (call: IE2ECall) => { + call.on('state-changed', (() => { + const state = call.state; + const stateStr = state.toString(); + setCallStatus(stateStr.charAt(0).toUpperCase() + stateStr.slice(1)); + + if (state === 'closed' || state === 'failed') { + setCallActive(false); + } + }) as any); + }; + + const handleJoinChannel = async (hash: string) => { + if (!chat) { + setSetupError('Chat not initialized'); + return; + } + + try { + const newUserId = (utils as any).generateUUID(); + setUserId(newUserId); + await chat.setChannel(hash, newUserId); + setChannelHash(hash); + setShowSetup(false); + setSetupError(''); + + // Check for existing users + const users = await chat.getUsersInChannel(); + if (users && users.length > 1) { + setPeerConnected(true); + } + + // Update URL hash + window.location.hash = hash; + } catch (err) { + console.error('Join error:', err); + setSetupError('Failed to connect.'); + throw err; + } + }; + + const sendMessage = useCallback( + async (text: string) => { + if (!chat || !text.trim()) return; + + setMessages((prev) => [ + ...prev, + { + sender: userId, + text, + type: 'sent', + timestamp: new Date(), + }, + ]); + + try { + await chat.encrypt({ image: '', text }).send(); + } catch (err) { + console.error('Send error:', err); + } + }, + [chat, userId] + ); + + const startCall = useCallback(async () => { + if (!chat) return; + + try { + const call = await chat.startCall(); + setCurrentCall(call); + setCallActive(true); + setCallStatus('Calling...'); + setupCallListeners(call); + } catch (err: any) { + alert(err.message); + } + }, [chat]); + + const endCall = useCallback(async () => { + if (currentCall) { + await currentCall.endCall(); + setCallActive(false); + setCurrentCall(null); + } + }, [currentCall]); + + return ( +
+ {showSetup ? ( + + ) : ( + + )} + {callActive && ( + + )} +
+ ); +} + +export default App; diff --git a/client/src/components/CallOverlay.tsx b/client/src/components/CallOverlay.tsx new file mode 100644 index 0000000..a08ac04 --- /dev/null +++ b/client/src/components/CallOverlay.tsx @@ -0,0 +1,65 @@ +import { useEffect, useState } from 'react'; + +interface CallOverlayProps { + status: string; + onEndCall: () => void; +} + +/** + * Call overlay component for audio call interface + * + * Manages: + * - Call status display + * - Call duration timer + * - End call functionality + * - Visual feedback during active calls + */ +function CallOverlay({ status, onEndCall }: CallOverlayProps) { + const [callDuration, setCallDuration] = useState('00:00'); + const [callStartTime, setCallStartTime] = useState(0); + + useEffect(() => { + if (status.toLowerCase() === 'connected') { + setCallStartTime(Date.now()); + } + }, [status]); + + useEffect(() => { + if (status.toLowerCase() === 'connected' && callStartTime > 0) { + const timer = setInterval(() => { + const seconds = Math.floor((Date.now() - callStartTime) / 1000); + const m = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const s = (seconds % 60).toString().padStart(2, '0'); + setCallDuration(`${m}:${s}`); + }, 1000); + + return () => clearInterval(timer); + } + }, [status, callStartTime]); + + return ( +
+
+
+

{status}

+

{callDuration}

+ +
+
+ ); +} + +export default CallOverlay; diff --git a/client/src/components/ChatContainer.tsx b/client/src/components/ChatContainer.tsx new file mode 100644 index 0000000..d3de38b --- /dev/null +++ b/client/src/components/ChatContainer.tsx @@ -0,0 +1,149 @@ +import { useState, useRef, useEffect } from 'react'; +import Message from './Message'; + +interface MessageType { + sender: string; + text: string; + type: 'sent' | 'received'; + timestamp: Date; +} + +interface ChatContainerProps { + channelHash: string; + messages: MessageType[]; + peerConnected: boolean; + onSendMessage: (text: string) => void; + onStartCall: () => void; +} + +/** + * Main chat container component + * + * Displays: + * - Chat header with connection status and channel hash + * - Message list + * - Message input and send functionality + * - Audio call button + */ +function ChatContainer({ + channelHash, + messages, + peerConnected, + onSendMessage, + onStartCall, +}: ChatContainerProps) { + const [inputValue, setInputValue] = useState(''); + const messagesEndRef = useRef(null); + + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + const handleSendMessage = () => { + if (inputValue.trim()) { + onSendMessage(inputValue); + setInputValue(''); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter') { + handleSendMessage(); + } + }; + + const handleCopyHash = () => { + navigator.clipboard.writeText(channelHash); + }; + + return ( +
+
+
+
+ +

Secure Channel

+ E2EE +
+
+ {channelHash} + +
+

+ {peerConnected + ? 'Peer joined. Communication is encrypted.' + : 'Waiting for someone to join...'} +

+
+
+ +
+
+ +
+ {messages.map((message, index) => ( + + ))} +
+
+ +
+
+ setInputValue(e.target.value)} + onKeyPress={handleKeyPress} + placeholder="Type a secure message..." + /> + +
+
+
+ ); +} + +export default ChatContainer; diff --git a/client/src/components/Message.tsx b/client/src/components/Message.tsx new file mode 100644 index 0000000..9704df6 --- /dev/null +++ b/client/src/components/Message.tsx @@ -0,0 +1,36 @@ +interface MessageProps { + message: { + sender: string; + text: string; + type: 'sent' | 'received'; + timestamp: Date; + }; +} + +/** + * Individual message component + * + * Displays a single chat message with: + * - Message text + * - Sender information + * - Timestamp + * - Styling based on sent/received type + */ +function Message({ message }: MessageProps) { + const time = message.timestamp.toLocaleTimeString([], { + hour: '2-digit', + minute: '2-digit', + }); + + return ( +
+
{message.text}
+
+ {message.sender} + {time} +
+
+ ); +} + +export default Message; diff --git a/client/src/components/SetupOverlay.tsx b/client/src/components/SetupOverlay.tsx new file mode 100644 index 0000000..ac9acb8 --- /dev/null +++ b/client/src/components/SetupOverlay.tsx @@ -0,0 +1,163 @@ +import { useState, useEffect } from 'react'; +import type { IChatE2EE } from '@chat-e2ee/service'; + +interface SetupOverlayProps { + chat: IChatE2EE | null; + onJoinChannel: (hash: string) => Promise; + error: string; +} + +type ViewType = 'initial' | 'create' | 'join'; + +/** + * Setup overlay component for channel creation and joining + * + * Handles the initial setup flow: + * - Creating a new channel with generated hash + * - Joining an existing channel with user-provided hash + * - URL hash parameter handling for direct joins + */ +function SetupOverlay({ chat, onJoinChannel, error }: SetupOverlayProps) { + const [view, setView] = useState('initial'); + const [generatedHash, setGeneratedHash] = useState(''); + const [enteredHash, setEnteredHash] = useState(''); + const [statusMessage, setStatusMessage] = useState(''); + const [isJoining, setIsJoining] = useState(false); + + // Check for URL hash on mount + useEffect(() => { + const hash = window.location.hash.replace('#', ''); + if (hash && hash.length > 5) { + setEnteredHash(hash); + setView('join'); + } + }, []); + + const handleShowCreate = async () => { + if (!chat) return; + + setView('create'); + try { + setGeneratedHash('Generating...'); + const linkObj = await chat.getLink(); + setGeneratedHash(linkObj.hash); + } catch (err) { + setStatusMessage('Failed to generate hash.'); + } + }; + + const handleShowJoin = () => { + setView('join'); + }; + + const handleBack = () => { + setView('initial'); + setGeneratedHash(''); + setEnteredHash(''); + setStatusMessage(''); + }; + + const handleCopyHash = () => { + navigator.clipboard.writeText(generatedHash); + const originalMessage = statusMessage; + setStatusMessage('Hash copied to clipboard!'); + setTimeout(() => setStatusMessage(originalMessage), 2000); + }; + + const handleJoin = async () => { + const finalHash = enteredHash || generatedHash; + + if (!finalHash) { + setStatusMessage('Please enter or generate a hash.'); + return; + } + + try { + setIsJoining(true); + setStatusMessage('Connecting...'); + await onJoinChannel(finalHash); + } catch (err) { + setIsJoining(false); + // Error is handled in parent + } + }; + + return ( +
+
+

Secure Messenger

+

Simple. End-to-End Encrypted. Private.

+ + {view === 'initial' && ( +
+ + +
+ )} + + {view === 'create' && ( + <> +
+ +
+ + +
+
+ + )} + + {view === 'join' && ( +
+ + setEnteredHash(e.target.value)} + placeholder="Enter hash to join..." + /> +
+ )} + + {view !== 'initial' && ( +
+ + +
+ )} + + {(statusMessage || error) && ( +
{error || statusMessage}
+ )} +
+
+ ); +} + +export default SetupOverlay; diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..1842541 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,9 @@ +import { StrictMode } from 'react'; +import ReactDOM from 'react-dom/client'; +import App from './App'; + +ReactDOM.createRoot(document.getElementById('root')!).render( + + + +); diff --git a/client/style.css b/client/src/style.css similarity index 100% rename from client/style.css rename to client/src/style.css diff --git a/client/tsconfig.json b/client/tsconfig.json index 684eee8..2a2ee80 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -15,6 +15,8 @@ "resolveJsonModule": true, "isolatedModules": true, "noEmit": true, + /* React */ + "jsx": "react-jsx", /* Linting */ "strict": true, "noUnusedLocals": true, @@ -23,6 +25,7 @@ }, "include": [ "src/**/*.ts", + "src/**/*.tsx", "app.ts" ] } \ No newline at end of file diff --git a/client/vite.config.ts b/client/vite.config.ts new file mode 100644 index 0000000..c26ef38 --- /dev/null +++ b/client/vite.config.ts @@ -0,0 +1,13 @@ +import { defineConfig } from 'vite'; +import react from '@vitejs/plugin-react'; + +// https://vitejs.dev/config/ +export default defineConfig({ + plugins: [react()], + server: { + port: 3000, + }, + build: { + outDir: 'build', + }, +});