From 9c1f01b3ee1053b48ed6450e7f7f3cd538321552 Mon Sep 17 00:00:00 2001 From: VishnuGadekar7 Date: Tue, 7 Apr 2026 13:18:25 +0530 Subject: [PATCH 1/2] Merged Client from Vanilla HTML JS to React 18 Components --- client/README.md | 226 ++++++++++ client/index.html | 129 +----- client/package.json | 9 +- client/src/App.tsx | 62 +++ .../components/CallOverlay/CallOverlay.css | 90 ++++ .../components/CallOverlay/CallOverlay.tsx | 51 +++ .../ChatContainer/ChatContainer.css | 30 ++ .../ChatContainer/ChatContainer.tsx | 43 ++ .../components/ChatContainer/ChatFooter.css | 57 +++ .../components/ChatContainer/ChatFooter.tsx | 65 +++ .../components/ChatContainer/ChatHeader.css | 112 +++++ .../components/ChatContainer/ChatHeader.tsx | 87 ++++ .../ChatContainer/MessageBubble.css | 62 +++ .../ChatContainer/MessageBubble.tsx | 24 + .../components/ChatContainer/MessagesArea.css | 51 +++ .../components/ChatContainer/MessagesArea.tsx | 35 ++ .../SetupOverlay/CreateHashView.css | 23 + .../SetupOverlay/CreateHashView.tsx | 67 +++ .../SetupOverlay/InitialActions.css | 9 + .../SetupOverlay/InitialActions.tsx | 25 ++ .../components/SetupOverlay/JoinHashView.css | 15 + .../components/SetupOverlay/JoinHashView.tsx | 52 +++ .../components/SetupOverlay/SetupOverlay.css | 83 ++++ .../components/SetupOverlay/SetupOverlay.tsx | 135 ++++++ client/src/components/common/Button.css | 120 +++++ client/src/components/common/Button.tsx | 37 ++ client/src/components/common/Input.css | 52 +++ client/src/components/common/Input.tsx | 31 ++ client/src/components/common/icons.tsx | 96 ++++ client/src/context/ChatContext.tsx | 204 +++++++++ client/src/hooks/useAudioNotification.ts | 14 + client/src/hooks/useCallTimer.ts | 45 ++ client/src/hooks/useUrlHash.ts | 29 ++ client/src/main.tsx | 17 + client/src/styles/global.css | 94 ++++ client/src/types/index.ts | 100 +++++ client/src/utils/audioNotification.ts | 25 ++ client/src/utils/callTimer.ts | 27 ++ client/src/utils/messageHandling.ts | 22 + client/tsconfig.json | 2 + client/vite.config.ts | 2 + package-lock.json | 420 ++++++++++++------ 42 files changed, 2604 insertions(+), 275 deletions(-) create mode 100644 client/README.md create mode 100644 client/src/App.tsx create mode 100644 client/src/components/CallOverlay/CallOverlay.css create mode 100644 client/src/components/CallOverlay/CallOverlay.tsx create mode 100644 client/src/components/ChatContainer/ChatContainer.css create mode 100644 client/src/components/ChatContainer/ChatContainer.tsx create mode 100644 client/src/components/ChatContainer/ChatFooter.css create mode 100644 client/src/components/ChatContainer/ChatFooter.tsx create mode 100644 client/src/components/ChatContainer/ChatHeader.css create mode 100644 client/src/components/ChatContainer/ChatHeader.tsx create mode 100644 client/src/components/ChatContainer/MessageBubble.css create mode 100644 client/src/components/ChatContainer/MessageBubble.tsx create mode 100644 client/src/components/ChatContainer/MessagesArea.css create mode 100644 client/src/components/ChatContainer/MessagesArea.tsx create mode 100644 client/src/components/SetupOverlay/CreateHashView.css create mode 100644 client/src/components/SetupOverlay/CreateHashView.tsx create mode 100644 client/src/components/SetupOverlay/InitialActions.css create mode 100644 client/src/components/SetupOverlay/InitialActions.tsx create mode 100644 client/src/components/SetupOverlay/JoinHashView.css create mode 100644 client/src/components/SetupOverlay/JoinHashView.tsx create mode 100644 client/src/components/SetupOverlay/SetupOverlay.css create mode 100644 client/src/components/SetupOverlay/SetupOverlay.tsx create mode 100644 client/src/components/common/Button.css create mode 100644 client/src/components/common/Button.tsx create mode 100644 client/src/components/common/Input.css create mode 100644 client/src/components/common/Input.tsx create mode 100644 client/src/components/common/icons.tsx create mode 100644 client/src/context/ChatContext.tsx create mode 100644 client/src/hooks/useAudioNotification.ts create mode 100644 client/src/hooks/useCallTimer.ts create mode 100644 client/src/hooks/useUrlHash.ts create mode 100644 client/src/main.tsx create mode 100644 client/src/styles/global.css create mode 100644 client/src/types/index.ts create mode 100644 client/src/utils/audioNotification.ts create mode 100644 client/src/utils/callTimer.ts create mode 100644 client/src/utils/messageHandling.ts diff --git a/client/README.md b/client/README.md new file mode 100644 index 0000000..89647bd --- /dev/null +++ b/client/README.md @@ -0,0 +1,226 @@ +# Chat E2EE Client - React.js Implementation + +This is the React.js implementation of the Chat E2EE client, migrated from vanilla TypeScript while maintaining full functional parity with the original. + +## ๐Ÿ“‹ Project Structure + +``` +src/ +โ”œโ”€โ”€ components/ # All React components +โ”‚ โ”œโ”€โ”€ common/ # Reusable UI components (Button, Input, Icons) +โ”‚ โ”œโ”€โ”€ SetupOverlay/ # Channel creation/joining flow +โ”‚ โ”œโ”€โ”€ ChatContainer/ # Main chat interface +โ”‚ โ””โ”€โ”€ CallOverlay/ # Audio call UI +โ”œโ”€โ”€ context/ # React Context for global state +โ”‚ โ””โ”€โ”€ ChatContext.tsx # Chat service wrapper and state management +โ”œโ”€โ”€ hooks/ # Custom React hooks +โ”‚ โ”œโ”€โ”€ useCallTimer.ts +โ”‚ โ”œโ”€โ”€ useUrlHash.ts +โ”‚ โ””โ”€โ”€ useAudioNotification.ts +โ”œโ”€โ”€ types/ # TypeScript type definitions +โ”œโ”€โ”€ utils/ # Utility functions +โ”‚ โ”œโ”€โ”€ audioNotification.ts +โ”‚ โ”œโ”€โ”€ messageHandling.ts +โ”‚ โ””โ”€โ”€ callTimer.ts +โ”œโ”€โ”€ styles/ # Global styles +โ”‚ โ””โ”€โ”€ global.css +โ”œโ”€โ”€ App.tsx # Main application component +โ””โ”€โ”€ main.tsx # React application entry point +``` + +## ๐Ÿš€ Getting Started + +### Prerequisites +- Node.js (v16+) +- npm or yarn + +### Installation + +```bash +cd client +npm install +``` + +### Development Server + +```bash +npm run dev +``` + +The application will be available at `http://localhost:5173` + +### Building for Production + +```bash +npm run build +``` + +The compiled output will be in the `dist/` folder. + +### Preview Production Build + +```bash +npm run preview +``` + +## ๐Ÿ—๏ธ Architecture + +### State Management +- **ChatContext**: Wraps the `@chat-e2ee/service` package without any modifications +- Global state includes: connection status, messages, call status, user ID, and channel hash +- All service initialization and event handling is managed through React hooks + +### Component Hierarchy + +``` +App +โ”œโ”€โ”€ ChatProvider (Context) +โ”‚ โ”œโ”€โ”€ SetupOverlay +โ”‚ โ”‚ โ”œโ”€โ”€ InitialActions +โ”‚ โ”‚ โ”œโ”€โ”€ CreateHashView +โ”‚ โ”‚ โ””โ”€โ”€ JoinHashView +โ”‚ โ””โ”€โ”€ ChatContainer +โ”‚ โ”œโ”€โ”€ ChatHeader +โ”‚ โ”œโ”€โ”€ MessagesArea +โ”‚ โ”‚ โ””โ”€โ”€ MessageBubble (repeated) +โ”‚ โ”œโ”€โ”€ ChatFooter +โ”‚ โ””โ”€โ”€ CallOverlay +``` + +### Key Features +- โœ… Full end-to-end message encryption +- โœ… Audio call support +- โœ… Real-time peer detection +- โœ… Glass-morphism UI design +- โœ… Mobile-responsive layout +- โœ… Native share API integration +- โœ… URL hash auto-population for channel joining + +## ๐Ÿ”’ Security & Backend Integration + +**Important:** All backend communication and encryption logic is handled by the `@chat-e2ee/service` package. This client implementation: +- โœ… Does NOT modify any service logic +- โœ… Does NOT change API contracts +- โœ… Only wraps the service with React state management +- โœ… Preserves all encryption/decryption calls + +Backend environment variable: +```bash +CHATE2EE_API_URL=http://localhost:3001 # or your backend URL +``` + +## ๐Ÿ“ฑ Responsive Design + +The UI is fully responsive and tested on: +- Desktop browsers (1920x1080+) +- Tablets (768px+) +- Mobile phones (320px+) +- iOS Safe Area support for notches + +## ๐Ÿ”„ Migration Notes + +This is a pure UI layer refactoring from vanilla TypeScript to React.js: + +**What Changed:** +- DOM manipulation replaced with React components +- Event listeners replaced with React hooks and Context +- Global variables replaced with React state +- CSS organization improved with component-scoped styling + +**What Stayed the Same:** +- All service integration unchanged +- All encryption logic unchanged +- All API calls unchanged +- All user-facing functionality identical +- All UI/UX identical + +## ๐Ÿงช Testing + +### Manual Testing Checklist + +- [ ] Create new channel flow +- [ ] Join existing channel flow +- [ ] Send/receive messages in real-time +- [ ] Audio call initiation and termination +- [ ] Copy hash functionality +- [ ] URL hash auto-population +- [ ] Peer detection and status indicators +- [ ] Mobile responsiveness +- [ ] Message animations +- [ ] Call duration timer + +### Browser Support + +- Chrome/Chromium 90+ +- Firefox 88+ +- Safari 14+ +- Edge 90+ + +## ๐Ÿ› ๏ธ Development + +### Adding a New Component + +1. Create component directory in `src/components/` +2. Add component file (`.tsx`) and styles file (`.css`) +3. Export from component file using named export +4. Import and use in parent component + +### Adding a New Hook + +1. Create hook file in `src/hooks/` +2. Use React hooks (useState, useEffect, useCallback, etc.) +3. Export custom hook with `use` prefix +4. Import in components that need it + +### Styling Guidelines + +- Use CSS classes for styling (not inline styles) +- Follow BEM-like naming: `component-name`, `component-name__element` +- Import CSS in component file for scoped styling +- Use CSS variables from `styles/global.css` for consistency +- Maintain mobile-first responsive design + +## ๐Ÿ“ฆ Dependencies + +- `react@^18.2.0` - React library +- `react-dom@^18.2.0` - React DOM rendering +- `@chat-e2ee/service@*` - E2EE messaging service (unmodified) +- `typescript@^5.6.2` - TypeScript compiler +- `vite@^5.4.1` - Build tool + +## ๐Ÿ› Troubleshooting + +### Chat not initializing +- Check `CHATE2EE_API_URL` environment variable +- Verify backend server is running +- Check browser console for detailed errors + +### Messages not sending +- Ensure peer has joined the channel +- Check network connection +- Verify encryption keys are initialized + +### Build errors +- Run `npm install` to ensure all dependencies are installed +- Check Node.js version (requires v16+) +- Clear `node_modules` and reinstall if needed + +## ๐Ÿ“„ License + +Same as the main Chat E2EE project. + +## ๐Ÿค Contributing + +This is part of the open-source Chat E2EE project. When contributing: + +1. **Only modify files within the `client/` folder** +2. **Do not change `@chat-e2ee/service` imports or behavior** +3. **Maintain full backward compatibility with the service** +4. **Test all features before submitting PR** +5. **Follow the existing code style and component patterns** + +For specific contribution guidelines, see the main project README. + +--- + +Made with โค๏ธ for secure, private communication diff --git a/client/index.html b/client/index.html index 03d8a4a..1fab440 100644 --- a/client/index.html +++ b/client/index.html @@ -5,138 +5,15 @@ Chat E2EE - Minimal & Secure - -
- -
-
-

Secure Messenger

-

Simple. End-to-End Encrypted. Private.

- -
- - -
- - - - - - - -
-
-
- - - - - - -
- + +
+ \ No newline at end of file diff --git a/client/package.json b/client/package.json index 80b81b1..b3dcd3b 100644 --- a/client/package.json +++ b/client/package.json @@ -11,9 +11,14 @@ }, "devDependencies": { "typescript": "^5.6.2", - "vite": "^5.4.1" + "vite": "^5.4.1", + "@vitejs/plugin-react": "^4.2.1", + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15" }, "dependencies": { - "@chat-e2ee/service": "*" + "@chat-e2ee/service": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" } } \ No newline at end of file diff --git a/client/src/App.tsx b/client/src/App.tsx new file mode 100644 index 0000000..e6032ae --- /dev/null +++ b/client/src/App.tsx @@ -0,0 +1,62 @@ +/** + * Main App component + */ + +import React, { useEffect, useState } from 'react'; +import { useChat } from './context/ChatContext'; +import { SetupOverlay } from './components/SetupOverlay/SetupOverlay'; +import { ChatContainer } from './components/ChatContainer/ChatContainer'; +import { updateUrlHash } from './utils/callTimer'; +import './styles/global.css'; + +const AppContent: React.FC = () => { + const { initializeChat, joinChannel, channelHash } = useChat(); + const [showSetup, setShowSetup] = useState(true); + const [error, setError] = useState(''); + + // Initialize chat on mount + useEffect(() => { + initializeChat().catch((err) => { + setError('Failed to initialize chat. Please refresh the page.'); + console.error('Initialization error:', err); + }); + }, [initializeChat]); + + const handleSetupComplete = async (hash: string) => { + try { + setError(''); + await joinChannel(hash); + updateUrlHash(hash); + setShowSetup(false); + } catch (err) { + setError((err as any).message || 'Failed to connect. Please try again.'); + console.error('Setup error:', err); + } + }; + + return ( + <> + + + {error && ( +
+ {error} +
+ )} + + ); +}; + +export default AppContent; diff --git a/client/src/components/CallOverlay/CallOverlay.css b/client/src/components/CallOverlay/CallOverlay.css new file mode 100644 index 0000000..eb7837c --- /dev/null +++ b/client/src/components/CallOverlay/CallOverlay.css @@ -0,0 +1,90 @@ +/** + * Call overlay styles + */ + +.blur-overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 200; + display: flex; + justify-content: center; + align-items: center; + background: rgba(15, 23, 42, 0.8); + padding: 1rem; + padding-bottom: calc(1rem + env(safe-area-inset-bottom)); + animation: fadeIn 0.3s ease; +} + +@keyframes fadeIn { + from { + opacity: 0; + } + + to { + opacity: 1; + } +} + +.call-info { + text-align: center; +} + +.call-avatar { + width: 120px; + height: 120px; + border-radius: 50%; + background: var(--glass); + margin: 0 auto 1.5rem; + border: 2px solid var(--primary); +} + +.shimmer { + background: linear-gradient(90deg, var(--glass) 25%, rgba(99, 102, 241, 0.2) 50%, var(--glass) 75%); + background-size: 200% 100%; + animation: shimmer 1.5s infinite; +} + +@keyframes shimmer { + 0% { + background-position: 200% 0; + } + + 100% { + background-position: -200% 0; + } +} + +.call-status { + font-size: 1.5rem; + margin-bottom: 0.5rem; + color: white; +} + +.call-duration { + color: var(--text-muted); + margin-bottom: 2rem; + font-size: 1.1rem; +} + +/* Mobile responsive */ +@media (max-width: 480px) { + .blur-overlay { + padding: 1rem; + } + + .call-avatar { + width: 100px; + height: 100px; + } + + .call-status { + font-size: 1.25rem; + } + + .call-duration { + margin-bottom: 1.5rem; + } +} \ No newline at end of file diff --git a/client/src/components/CallOverlay/CallOverlay.tsx b/client/src/components/CallOverlay/CallOverlay.tsx new file mode 100644 index 0000000..8de3cbd --- /dev/null +++ b/client/src/components/CallOverlay/CallOverlay.tsx @@ -0,0 +1,51 @@ +/** + * Call overlay component + */ + +import React, { useEffect } from 'react'; +import { useChat } from '../../context/ChatContext'; +import { useCallTimer } from '../../hooks/useCallTimer'; +import { Button } from '../common/Button'; +import { EndCallIcon } from '../common/icons'; +import './CallOverlay.css'; + +export const CallOverlay: React.FC = () => { + const { callActive, callStatus, endCall } = useChat(); + const { duration, formatDuration, startTimer } = useCallTimer(); + + useEffect(() => { + if (callActive && callStatus === 'Connected') { + startTimer(); + } + }, [callActive, callStatus, startTimer]); + + if (!callActive) return null; + + const handleEndCall = async () => { + await endCall(); + }; + + return ( +
+
+
+

+ {callStatus || 'Calling...'} +

+

+ {formatDuration(duration)} +

+ +
+
+ ); +}; diff --git a/client/src/components/ChatContainer/ChatContainer.css b/client/src/components/ChatContainer/ChatContainer.css new file mode 100644 index 0000000..9feae0c --- /dev/null +++ b/client/src/components/ChatContainer/ChatContainer.css @@ -0,0 +1,30 @@ +/** + * Chat container styles + */ + +#chat-container { + width: 100%; + max-width: 800px; + height: 100%; + max-height: 800px; + display: flex; + flex-direction: column; + border-radius: 2rem; + overflow: hidden; + opacity: 1; + transition: all 0.3s ease; +} + +#chat-container.hidden { + display: none; + opacity: 0; +} + +/* Mobile responsive */ +@media (max-width: 480px) { + #chat-container { + max-width: 100%; + max-height: 100%; + border-radius: 0; + } +} \ No newline at end of file diff --git a/client/src/components/ChatContainer/ChatContainer.tsx b/client/src/components/ChatContainer/ChatContainer.tsx new file mode 100644 index 0000000..c6d7757 --- /dev/null +++ b/client/src/components/ChatContainer/ChatContainer.tsx @@ -0,0 +1,43 @@ +/** + * Main chat container component + */ + +import React, { useState } from 'react'; +import { useChat } from '../../context/ChatContext'; +import { ChatHeader } from './ChatHeader'; +import { MessagesArea } from './MessagesArea'; +import { ChatFooter } from './ChatFooter'; +import { CallOverlay } from '../CallOverlay/CallOverlay'; +import './ChatContainer.css'; + +interface ChatContainerProps { + isHidden: boolean; +} + +export const ChatContainer: React.FC = ({ isHidden }) => { + const { startCall } = useChat(); + const [isStartingCall, setIsStartingCall] = useState(false); + + const handleStartCall = async () => { + try { + setIsStartingCall(true); + await startCall(); + } catch (err) { + console.error('Failed to start call:', err); + alert((err as any).message || 'Failed to start call'); + } finally { + setIsStartingCall(false); + } + }; + + return ( + <> +
+ + + +
+ + + ); +}; diff --git a/client/src/components/ChatContainer/ChatFooter.css b/client/src/components/ChatContainer/ChatFooter.css new file mode 100644 index 0000000..a7c16b0 --- /dev/null +++ b/client/src/components/ChatContainer/ChatFooter.css @@ -0,0 +1,57 @@ +/** + * Chat footer styles + */ + +.chat-footer { + padding: 1.5rem; +} + +.input-container { + display: flex; + gap: 0.75rem; + align-items: center; +} + +.message-input { + flex: 1; + padding: 0.75rem 1rem; + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--glass-border); + border-radius: 0.75rem; + color: white; + font-family: inherit; + font-size: 1rem; + transition: all 0.2s; +} + +.message-input:focus { + outline: none; + border-color: var(--primary); + background: rgba(0, 0, 0, 0.4); +} + +.message-input::placeholder { + color: var(--text-muted); +} + +.message-input:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Mobile responsive */ +@media (max-width: 480px) { + .chat-footer { + padding: 0.75rem; + padding-bottom: calc(0.75rem + env(safe-area-inset-bottom)); + } + + .input-container { + gap: 0.5rem; + } + + .message-input { + font-size: 16px; + /* Prevents zoom on iOS */ + } +} \ No newline at end of file diff --git a/client/src/components/ChatContainer/ChatFooter.tsx b/client/src/components/ChatContainer/ChatFooter.tsx new file mode 100644 index 0000000..1ea9e83 --- /dev/null +++ b/client/src/components/ChatContainer/ChatFooter.tsx @@ -0,0 +1,65 @@ +/** + * Chat footer component (message input) + */ + +import React, { useState, useRef } from 'react'; +import { useChat } from '../../context/ChatContext'; +import { Button } from '../common/Button'; +import { SendIcon } from '../common/icons'; +import './ChatFooter.css'; + +export const ChatFooter: React.FC = () => { + const { sendMessage } = useChat(); + const [message, setMessage] = useState(''); + const [isSending, setIsSending] = useState(false); + const inputRef = useRef(null); + + const handleSend = async () => { + if (!message.trim()) return; + + try { + setIsSending(true); + await sendMessage(message); + setMessage(''); + inputRef.current?.focus(); + } catch (err) { + console.error('Failed to send message:', err); + } finally { + setIsSending(false); + } + }; + + const handleKeyPress = (e: React.KeyboardEvent) => { + if (e.key === 'Enter' && !e.shiftKey) { + e.preventDefault(); + handleSend(); + } + }; + + return ( +
+
+ setMessage(e.target.value)} + onKeyPress={handleKeyPress} + disabled={isSending} + /> + +
+
+ ); +}; diff --git a/client/src/components/ChatContainer/ChatHeader.css b/client/src/components/ChatContainer/ChatHeader.css new file mode 100644 index 0000000..661c3d8 --- /dev/null +++ b/client/src/components/ChatContainer/ChatHeader.css @@ -0,0 +1,112 @@ +/** + * Chat header styles + */ + +.chat-header { + padding: 1.5rem; + display: flex; + justify-content: space-between; + align-items: center; + gap: 1rem; + flex-wrap: wrap; +} + +.header-info { + flex: 1; +} + +.title-row { + display: flex; + align-items: center; + gap: 0.75rem; + margin-bottom: 0.25rem; +} + +.status-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-muted); + transition: all 0.3s ease; +} + +.status-dot.connected { + background: var(--success); + box-shadow: 0 0 8px var(--success); +} + +.channel-title { + font-size: 1.25rem; + font-weight: 600; +} + +.badge { + font-size: 0.7rem; + background: rgba(16, 185, 129, 0.2); + color: var(--success); + padding: 2px 8px; + border-radius: 1rem; + font-weight: 600; +} + +.hash-badge-container { + display: flex; + align-items: center; + gap: 0.5rem; + background: rgba(255, 255, 255, 0.05); + padding: 0.25rem 0.5rem; + border-radius: 0.5rem; + margin-top: 0.5rem; + width: fit-content; + border: 1px solid var(--glass-border); + position: relative; +} + +.hash-text { + font-family: monospace; + font-size: 0.75rem; + color: var(--text-muted); + word-break: break-all; +} + +.copy-feedback-small { + position: absolute; + top: -20px; + right: 0; + font-size: 0.75rem; + color: var(--success); + white-space: nowrap; +} + +.participant-info { + font-size: 0.8rem; + color: var(--text-muted); + margin-top: 0.25rem; +} + +.header-actions { + display: flex; + gap: 0.5rem; +} + +/* Mobile responsive */ +@media (max-width: 480px) { + .chat-header { + padding: 0.875rem 1rem; + padding-top: calc(0.875rem + env(safe-area-inset-top)); + flex-direction: column; + align-items: flex-start; + } + + .channel-title { + font-size: 1.05rem; + } + + .hash-badge-container { + display: none; + } + + .header-actions { + align-self: flex-end; + } +} \ No newline at end of file diff --git a/client/src/components/ChatContainer/ChatHeader.tsx b/client/src/components/ChatContainer/ChatHeader.tsx new file mode 100644 index 0000000..43a8b3b --- /dev/null +++ b/client/src/components/ChatContainer/ChatHeader.tsx @@ -0,0 +1,87 @@ +/** + * Chat header component + */ + +import React, { useState } from 'react'; +import { useChat } from '../../context/ChatContext'; +import { Button } from '../common/Button'; +import { CopyIcon, ShareIcon, PhoneIcon } from '../common/icons'; +import './ChatHeader.css'; + +interface ChatHeaderProps { + onStartCall: () => void; +} + +export const ChatHeader: React.FC = ({ onStartCall }) => { + const { isConnected, channelHash } = useChat(); + const [hashCopied, setHashCopied] = useState(false); + + const handleCopyHash = () => { + navigator.clipboard.writeText(window.location.href); + setHashCopied(true); + setTimeout(() => setHashCopied(false), 2000); + }; + + const handleShare = () => { + if ('share' in navigator) { + navigator + .share({ + title: 'Chat E2EE', + text: 'Join my end-to-end encrypted chat', + url: window.location.href, + }) + .catch(() => { + /* user cancelled */ + }); + } + }; + + return ( +
+
+
+ +

Secure Channel

+ E2EE +
+ {channelHash && ( +
+ {channelHash} + + {hashCopied && Copied!} +
+ )} +

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

+
+
+ {typeof navigator !== 'undefined' && 'share' in navigator && ( + + )} + +
+
+ ); +}; diff --git a/client/src/components/ChatContainer/MessageBubble.css b/client/src/components/ChatContainer/MessageBubble.css new file mode 100644 index 0000000..4e7f773 --- /dev/null +++ b/client/src/components/ChatContainer/MessageBubble.css @@ -0,0 +1,62 @@ +/** + * Message bubble styles + */ + +.message { + max-width: 70%; + padding: 0.75rem 1rem; + border-radius: 1.25rem; + font-size: 0.95rem; + line-height: 1.4; + position: relative; + animation: slideUp 0.3s ease-out; +} + +@keyframes slideUp { + from { + opacity: 0; + transform: translateY(10px); + } + + to { + opacity: 1; + transform: translateY(0); + } +} + +.message.sent { + align-self: flex-end; + background: var(--primary); + border-bottom-right-radius: 0.25rem; + color: white; + margin-left: auto; +} + +.message.received { + align-self: flex-start; + background: var(--glass); + border: 1px solid var(--glass-border); + border-bottom-left-radius: 0.25rem; +} + +.message-text { + word-wrap: break-word; + overflow-wrap: break-word; +} + +.message-meta { + font-size: 0.7rem; + color: rgba(255, 255, 255, 0.5); + margin-top: 0.25rem; + display: flex; + justify-content: space-between; + gap: 0.5rem; +} + +/* Mobile responsive */ +@media (max-width: 480px) { + .message { + max-width: 85%; + font-size: 0.9rem; + } +} \ No newline at end of file diff --git a/client/src/components/ChatContainer/MessageBubble.tsx b/client/src/components/ChatContainer/MessageBubble.tsx new file mode 100644 index 0000000..c7d4932 --- /dev/null +++ b/client/src/components/ChatContainer/MessageBubble.tsx @@ -0,0 +1,24 @@ +/** + * Message bubble component + */ + +import React from 'react'; +import { Message } from '../../types/index'; +import { formatMessageTime } from '../../utils/messageHandling'; +import './MessageBubble.css'; + +interface MessageBubbleProps { + message: Message; +} + +export const MessageBubble: React.FC = ({ message }) => { + return ( +
+
{message.text}
+
+ {message.sender.substring(0, 8)}... + {formatMessageTime(message.timestamp)} +
+
+ ); +}; diff --git a/client/src/components/ChatContainer/MessagesArea.css b/client/src/components/ChatContainer/MessagesArea.css new file mode 100644 index 0000000..a2d0877 --- /dev/null +++ b/client/src/components/ChatContainer/MessagesArea.css @@ -0,0 +1,51 @@ +/** + * Messages area styles + */ + +.messages-area { + flex: 1; + overflow-y: auto; + padding: 1.5rem; + display: flex; + flex-direction: column; + gap: 1rem; + scroll-behavior: smooth; +} + +.empty-state { + display: flex; + align-items: center; + justify-content: center; + height: 100%; + color: var(--text-muted); +} + +.empty-state p { + text-align: center; +} + +/* Scrollbar styling */ +.messages-area::-webkit-scrollbar { + width: 8px; +} + +.messages-area::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); +} + +.messages-area::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.4); + border-radius: 4px; +} + +.messages-area::-webkit-scrollbar-thumb:hover { + background: rgba(99, 102, 241, 0.6); +} + +/* Mobile responsive */ +@media (max-width: 480px) { + .messages-area { + padding: 0.875rem 0.75rem; + gap: 0.75rem; + } +} \ No newline at end of file diff --git a/client/src/components/ChatContainer/MessagesArea.tsx b/client/src/components/ChatContainer/MessagesArea.tsx new file mode 100644 index 0000000..9150206 --- /dev/null +++ b/client/src/components/ChatContainer/MessagesArea.tsx @@ -0,0 +1,35 @@ +/** + * Messages area component + */ + +import React, { useEffect, useRef } from 'react'; +import { useChat } from '../../context/ChatContext'; +import { MessageBubble } from './MessageBubble'; +import './MessagesArea.css'; + +export const MessagesArea: React.FC = () => { + const { messages } = useChat(); + const messagesEndRef = useRef(null); + + // Auto-scroll to bottom on new messages + useEffect(() => { + messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); + }, [messages]); + + return ( +
+ {messages.length === 0 ? ( +
+

No messages yet. Start the conversation!

+
+ ) : ( + <> + {messages.map((message, index) => ( + + ))} +
+ + )} +
+ ); +}; diff --git a/client/src/components/SetupOverlay/CreateHashView.css b/client/src/components/SetupOverlay/CreateHashView.css new file mode 100644 index 0000000..81c6ea7 --- /dev/null +++ b/client/src/components/SetupOverlay/CreateHashView.css @@ -0,0 +1,23 @@ +/** + * Create hash view styles + */ + +.create-hash-view { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.copy-feedback { + font-size: 0.85rem; + color: var(--success); + margin-top: 0.5rem; + display: block; +} + +.button-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; + margin-top: 1rem; +} \ No newline at end of file diff --git a/client/src/components/SetupOverlay/CreateHashView.tsx b/client/src/components/SetupOverlay/CreateHashView.tsx new file mode 100644 index 0000000..5093f3c --- /dev/null +++ b/client/src/components/SetupOverlay/CreateHashView.tsx @@ -0,0 +1,67 @@ +/** + * Create hash view component + */ + +import React, { useState } from 'react'; +import { Input } from '../common/Input'; +import { Button } from '../common/Button'; +import { CopyIcon } from '../common/icons'; +import './CreateHashView.css'; + +interface CreateHashViewProps { + hash: string; + onHashGenerated: (hash: string) => void; + onCopyClick: () => void; + onBack: () => void; + onNext: () => void; +} + +export const CreateHashView: React.FC = ({ + hash, + onHashGenerated, + onCopyClick, + onBack, + onNext, +}) => { + const [copied, setCopied] = useState(false); + + const handleCopy = () => { + onCopyClick(); + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }; + + return ( +
+
+ +
+ { }} + placeholder="Generating..." + readOnly + /> + +
+ {copied && Copied!} +
+ +
+ + +
+
+ ); +}; diff --git a/client/src/components/SetupOverlay/InitialActions.css b/client/src/components/SetupOverlay/InitialActions.css new file mode 100644 index 0000000..51c03ca --- /dev/null +++ b/client/src/components/SetupOverlay/InitialActions.css @@ -0,0 +1,9 @@ +/** + * Initial actions styles + */ + +.initial-actions { + display: flex; + flex-direction: column; + gap: 1rem; +} \ No newline at end of file diff --git a/client/src/components/SetupOverlay/InitialActions.tsx b/client/src/components/SetupOverlay/InitialActions.tsx new file mode 100644 index 0000000..4754742 --- /dev/null +++ b/client/src/components/SetupOverlay/InitialActions.tsx @@ -0,0 +1,25 @@ +/** + * Initial actions for setup overlay + */ + +import React from 'react'; +import { Button } from '../common/Button'; +import './InitialActions.css'; + +interface InitialActionsProps { + onCreateClick: () => void; + onJoinClick: () => void; +} + +export const InitialActions: React.FC = ({ onCreateClick, onJoinClick }) => { + return ( +
+ + +
+ ); +}; diff --git a/client/src/components/SetupOverlay/JoinHashView.css b/client/src/components/SetupOverlay/JoinHashView.css new file mode 100644 index 0000000..a460154 --- /dev/null +++ b/client/src/components/SetupOverlay/JoinHashView.css @@ -0,0 +1,15 @@ +/** + * Join hash view styles + */ + +.join-hash-view { + display: flex; + flex-direction: column; + gap: 1.5rem; +} + +.button-group { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 1rem; +} \ No newline at end of file diff --git a/client/src/components/SetupOverlay/JoinHashView.tsx b/client/src/components/SetupOverlay/JoinHashView.tsx new file mode 100644 index 0000000..68bcd02 --- /dev/null +++ b/client/src/components/SetupOverlay/JoinHashView.tsx @@ -0,0 +1,52 @@ +/** + * Join hash view component + */ + +import React, { useEffect } from 'react'; +import { Input } from '../common/Input'; +import { Button } from '../common/Button'; +import { useUrlHash } from '../../hooks/useUrlHash'; +import './JoinHashView.css'; + +interface JoinHashViewProps { + hash: string; + onHashChange: (hash: string) => void; + onBack: () => void; + onJoin: () => void; +} + +export const JoinHashView: React.FC = ({ + hash, + onHashChange, + onBack, + onJoin, +}) => { + const { hash: urlHash } = useUrlHash(); + + // Auto-populate from URL if available + useEffect(() => { + if (urlHash && !hash) { + onHashChange(urlHash); + } + }, [urlHash, hash, onHashChange]); + + return ( +
+ + +
+ + +
+
+ ); +}; diff --git a/client/src/components/SetupOverlay/SetupOverlay.css b/client/src/components/SetupOverlay/SetupOverlay.css new file mode 100644 index 0000000..5743be4 --- /dev/null +++ b/client/src/components/SetupOverlay/SetupOverlay.css @@ -0,0 +1,83 @@ +/** + * SetupOverlay styles + */ + +.overlay { + position: fixed; + top: 0; + left: 0; + right: 0; + bottom: 0; + z-index: 100; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; + opacity: 1; + transition: opacity 0.3s ease; +} + +.overlay.hidden { + display: none; + opacity: 0; +} + +.overlay-content { + width: 100%; + max-width: 400px; + padding: 2.5rem; + border-radius: 2rem; + text-align: center; +} + +.overlay-content h1 { + font-size: 2rem; + margin-bottom: 0.5rem; + font-weight: 600; +} + +.overlay-content p { + color: var(--text-muted); + margin-bottom: 2rem; + font-size: 0.9rem; +} + +.setup-status { + margin-top: 1.5rem; + padding: 0.75rem 1rem; + background: rgba(99, 102, 241, 0.1); + border: 1px solid var(--primary); + border-radius: 0.5rem; + color: var(--primary); + font-size: 0.9rem; + text-align: center; +} + +.setup-status.error { + background: rgba(239, 68, 68, 0.1); + border-color: var(--danger); + color: var(--danger); +} + +/* Mobile responsive */ +@media (max-width: 480px) { + .overlay { + align-items: flex-end; + padding: 0; + } + + .overlay-content { + max-width: 100%; + border-radius: 1.75rem 1.75rem 0 0; + padding: 1.75rem 1.25rem; + padding-bottom: calc(1.75rem + env(safe-area-inset-bottom)); + } + + .overlay-content h1 { + font-size: 1.5rem; + } + + .overlay-content p { + font-size: 0.85rem; + } +} \ No newline at end of file diff --git a/client/src/components/SetupOverlay/SetupOverlay.tsx b/client/src/components/SetupOverlay/SetupOverlay.tsx new file mode 100644 index 0000000..1f7dcd7 --- /dev/null +++ b/client/src/components/SetupOverlay/SetupOverlay.tsx @@ -0,0 +1,135 @@ +/** + * Main SetupOverlay component + */ + +import React, { useState, useEffect } from 'react'; +import { useChat } from '../../context/ChatContext'; +import { InitialActions } from './InitialActions'; +import { CreateHashView } from './CreateHashView'; +import { JoinHashView } from './JoinHashView'; +import './SetupOverlay.css'; + +interface SetupOverlayProps { + onSetupComplete: (hash: string) => Promise; + isHidden: boolean; +} + +type ViewType = 'initial' | 'create' | 'join'; + +export const SetupOverlay: React.FC = ({ onSetupComplete, isHidden }) => { + const { createNewChannel } = useChat(); + const [view, setView] = useState('initial'); + const [generatedHash, setGeneratedHash] = useState(''); + const [joinHash, setJoinHash] = useState(''); + const [status, setStatus] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + // Generate hash when entering create view + useEffect(() => { + if (view === 'create' && !generatedHash) { + generateHash(); + } + }, [view]); // eslint-disable-line react-hooks/exhaustive-deps + + const generateHash = async () => { + try { + setStatus('Generating secure hash...'); + const hash = await createNewChannel(); + setGeneratedHash(hash); + setStatus(''); + } catch (err) { + setStatus('Failed to generate hash. Please try again.'); + console.error('Hash generation error:', err); + } + }; + + const handleCreateClick = () => { + setView('create'); + }; + + const handleJoinClick = () => { + setView('join'); + }; + + const handleBack = () => { + setView('initial'); + setGeneratedHash(''); + setJoinHash(''); + setStatus(''); + }; + + const handleCopyHash = () => { + navigator.clipboard.writeText(generatedHash); + }; + + const handleCreateNext = async () => { + if (!generatedHash) { + setStatus('Please generate a hash first.'); + return; + } + try { + setIsLoading(true); + setStatus('Connecting...'); + await onSetupComplete(generatedHash); + } catch (err) { + setStatus('Failed to connect. Please try again.'); + console.error('Setup error:', err); + } finally { + setIsLoading(false); + } + }; + + const handleJoinNext = async () => { + if (!joinHash.trim()) { + setStatus('Please enter a hash.'); + return; + } + try { + setIsLoading(true); + setStatus('Connecting...'); + await onSetupComplete(joinHash); + } catch (err) { + setStatus('Failed to join channel. Please check the hash and try again.'); + console.error('Join error:', err); + } finally { + setIsLoading(false); + } + }; + + return ( +
+
+

Secure Messenger

+

Simple. End-to-End Encrypted. Private.

+ + {view === 'initial' && ( + + )} + + {view === 'create' && ( + + )} + + {view === 'join' && ( + + )} + + {status &&
{status}
} +
+
+ ); +}; diff --git a/client/src/components/common/Button.css b/client/src/components/common/Button.css new file mode 100644 index 0000000..e186a74 --- /dev/null +++ b/client/src/components/common/Button.css @@ -0,0 +1,120 @@ +/** + * Button component styles + */ + +.btn { + padding: 0.75rem 1.5rem; + border-radius: 0.75rem; + font-family: inherit; + font-weight: 600; + cursor: pointer; + transition: all 0.2s; + border: none; + font-size: 1rem; +} + +.btn:hover:not(:disabled) { + transform: translateY(-2px); +} + +.btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Variants */ +.btn--primary { + background: var(--primary); + color: white; +} + +.btn--primary:hover:not(:disabled) { + background: var(--primary-hover); +} + +.btn--secondary { + background: var(--glass); + color: white; + border: 1px solid var(--glass-border); +} + +.btn--secondary:hover:not(:disabled) { + background: rgba(255, 255, 255, 0.1); +} + +.btn--danger { + background: var(--danger); + color: white; +} + +.btn--danger:hover:not(:disabled) { + background: #dc2626; +} + +/* Sizes */ +.btn--small { + padding: 0.5rem; + font-size: 0.875rem; +} + +.btn--large { + padding: 1.25rem 2rem; + font-size: 1.1rem; +} + +/* Circle button */ +.btn--circle { + border-radius: 50%; + width: 48px; + height: 48px; + padding: 0; + display: flex; + justify-content: center; + align-items: center; +} + +.btn--circle.btn--large { + width: 64px; + height: 64px; +} + +.btn svg { + width: 20px; + height: 20px; +} + +.btn--circle svg { + width: 24px; + height: 24px; +} + +.btn--circle.btn--large svg { + width: 32px; + height: 32px; +} + +/* Icon button */ +.btn--icon { + background: none; + color: var(--text-muted); + padding: 8px; +} + +.btn--icon:hover:not(:disabled) { + color: var(--text); + transform: none; +} + +.btn--icon svg { + width: 20px; + height: 20px; +} + +.btn--icon.btn--tiny { + padding: 2px; +} + +.btn--icon.btn--tiny svg { + width: 14px; + height: 14px; +} \ No newline at end of file diff --git a/client/src/components/common/Button.tsx b/client/src/components/common/Button.tsx new file mode 100644 index 0000000..eca433d --- /dev/null +++ b/client/src/components/common/Button.tsx @@ -0,0 +1,37 @@ +/** + * Reusable Button component + */ + +import React from 'react'; +import { ButtonProps } from '../../types/index'; +import './Button.css'; + +export const Button: React.FC = ({ + variant = 'primary', + size = 'medium', + onClick, + disabled = false, + circle = false, + children, + className = '', + title, +}) => { + const baseClass = 'btn'; + const variantClass = `btn--${variant}`; + const sizeClass = size ? `btn--${size}` : ''; + const circleClass = circle ? 'btn--circle' : ''; + + const classes = `${baseClass} ${variantClass} ${sizeClass} ${circleClass} ${className}`.trim(); + + return ( + + ); +}; diff --git a/client/src/components/common/Input.css b/client/src/components/common/Input.css new file mode 100644 index 0000000..ab41d13 --- /dev/null +++ b/client/src/components/common/Input.css @@ -0,0 +1,52 @@ +/** + * Input component styles + */ + +.input-group { + text-align: left; + margin-bottom: 1.25rem; +} + +.input-label { + display: block; + font-size: 0.75rem; + text-transform: uppercase; + letter-spacing: 0.05em; + color: var(--text-muted); + margin-bottom: 0.5rem; + font-weight: 600; +} + +.input-field { + width: 100%; + padding: 0.75rem 1rem; + background: rgba(0, 0, 0, 0.2); + border: 1px solid var(--glass-border); + border-radius: 0.75rem; + color: white; + font-family: inherit; + font-size: 1rem; + transition: all 0.2s; +} + +.input-field:focus { + outline: none; + border-color: var(--primary); + background: rgba(0, 0, 0, 0.4); +} + +.input-field:read-only { + cursor: not-allowed; + opacity: 0.8; +} + +/* Copy input container */ +.copy-input { + display: flex; + gap: 0.5rem; + align-items: center; +} + +.copy-input .input-field { + flex: 1; +} \ No newline at end of file diff --git a/client/src/components/common/Input.tsx b/client/src/components/common/Input.tsx new file mode 100644 index 0000000..04a5901 --- /dev/null +++ b/client/src/components/common/Input.tsx @@ -0,0 +1,31 @@ +/** + * Reusable Input component + */ + +import React from 'react'; +import { InputProps } from '../../types/index'; +import './Input.css'; + +export const Input: React.FC = ({ + label, + placeholder, + value, + onChange, + readOnly = false, + type = 'text', + className = '', +}) => { + return ( +
+ {label && } + onChange(e.target.value)} + readOnly={readOnly} + /> +
+ ); +}; diff --git a/client/src/components/common/icons.tsx b/client/src/components/common/icons.tsx new file mode 100644 index 0000000..c4a6cba --- /dev/null +++ b/client/src/components/common/icons.tsx @@ -0,0 +1,96 @@ +/** + * Icon components + */ + +import React from 'react'; + +interface IconProps { + size?: number; + className?: string; +} + +export const CopyIcon: React.FC = ({ size = 24, className = '' }) => ( + + + + +); + +export const ShareIcon: React.FC = ({ size = 24, className = '' }) => ( + + + + + + + +); + +export const PhoneIcon: React.FC = ({ size = 24, className = '' }) => ( + + + +); + +export const SendIcon: React.FC = ({ size = 24, className = '' }) => ( + + + + +); + +export const EndCallIcon: React.FC = ({ size = 24, className = '' }) => ( + + + +); diff --git a/client/src/context/ChatContext.tsx b/client/src/context/ChatContext.tsx new file mode 100644 index 0000000..31949e9 --- /dev/null +++ b/client/src/context/ChatContext.tsx @@ -0,0 +1,204 @@ +/** + * Chat context provider - wraps the existing @chat-e2ee/service + * No modifications to the service itself + */ + +import React, { createContext, useContext, ReactNode, useState, useEffect, useCallback } from 'react'; +import { createChatInstance, utils } from '@chat-e2ee/service'; +import { ChatContextType, ChatInstance, Message } from '../types/index'; +import { createMessage } from '../utils/messageHandling'; +import { playBeep } from '../utils/audioNotification'; + +const ChatContext = createContext(undefined); + +export const ChatProvider: React.FC<{ children: ReactNode }> = ({ children }) => { + const [chat, setChat] = useState(null); + const [userId, setUserId] = useState(''); + const [channelHash, setChannelHash] = useState(''); + const [privateKey, setPrivateKey] = useState(''); + const [messages, setMessages] = useState([]); + const [isConnected, setIsConnected] = useState(false); + const [callActive, setCallActive] = useState(false); + const [callStatus, setCallStatus] = useState(''); + const [callDuration, setCallDuration] = useState(0); + + // Initialize chat service (no modifications) + const initializeChat = useCallback(async () => { + try { + const chatInstance = createChatInstance({ + baseUrl: process.env.CHATE2EE_API_URL || 'http://localhost:3001', + }); + await chatInstance.init(); + + const keys = chatInstance.getKeyPair(); + setPrivateKey(keys.privateKey); + setChat(chatInstance); + } catch (err) { + console.error('Chat initialization failed:', err); + throw err; + } + }, []); + + // Create new channel + const createNewChannel = useCallback(async (): Promise => { + if (!chat) throw new Error('Chat not initialized'); + try { + const linkObj = await chat.getLink(); + return linkObj.hash; + } catch (err) { + console.error('Failed to create channel:', err); + throw err; + } + }, [chat]); + + // Join existing channel + const joinChannel = useCallback( + async (hash: string) => { + if (!chat) throw new Error('Chat not initialized'); + try { + // Auto-generate User ID + const newUserId = (utils as any).generateUUID(); + setUserId(newUserId); + + await chat.setChannel(hash, newUserId); + setChannelHash(hash); + setIsConnected(true); + + // Setup listeners + setupChatListeners(chat, newUserId); + + // Check for existing users + await checkExistingUsers(chat); + } catch (err) { + console.error('Failed to join channel:', err); + throw err; + } + }, + [chat] + ); + + // Send message + const sendMessage = useCallback( + async (text: string) => { + if (!chat || !userId) throw new Error('Chat not ready'); + try { + const message = createMessage(userId, text, 'sent'); + addMessage(message); + await chat.encrypt({ text }).send(); + } catch (err) { + console.error('Failed to send message:', err); + throw err; + } + }, + [chat, userId] + ); + + // Start call + const startCall = useCallback(async () => { + if (!chat) throw new Error('Chat not initialized'); + try { + const call = await chat.startCall(); + setCallActive(true); + setupCallListeners(call); + } catch (err) { + console.error('Failed to start call:', err); + throw err; + } + }, [chat]); + + // End call + const endCall = useCallback(async () => { + setCallActive(false); + setCallDuration(0); + }, []); + + // Add message to state + const addMessage = useCallback((message: Message) => { + setMessages((prev) => [...prev, message]); + }, []); + + // Setup chat listeners + const setupChatListeners = (chatInstance: ChatInstance, currentUserId: string) => { + chatInstance.on('on-alice-join', () => { + playBeep(); + setIsConnected(true); + }); + + chatInstance.on('on-alice-disconnect', () => { + setIsConnected(false); + }); + + chatInstance.on('chat-message', async (msg: any) => { + try { + const plainText = await (utils as any).decryptMessage(msg.message, privateKey); + const message = createMessage(msg.sender, plainText, 'received'); + addMessage(message); + } catch (err) { + console.error('Failed to decrypt message:', err); + } + }); + + chatInstance.on('call-added', (call: any) => { + setCallActive(true); + setCallStatus('Incoming Call...'); + setupCallListeners(call); + }); + }; + + // Setup call listeners + const setupCallListeners = (call: any) => { + call.on('state-changed', (state: string) => { + setCallStatus(state.charAt(0).toUpperCase() + state.slice(1)); + + if (state === 'closed' || state === 'failed') { + setCallActive(false); + setCallDuration(0); + setCallStatus(''); + } + }); + }; + + // Check for existing users + const checkExistingUsers = async (chatInstance: ChatInstance) => { + try { + const users = await chatInstance.getUsersInChannel(); + if (users && users.length > 1) { + playBeep(); + setIsConnected(true); + } + } catch (err) { + console.error('Error checking users:', err); + } + }; + + const value: ChatContextType = { + chat, + userId, + channelHash, + privateKey, + messages, + isConnected, + callActive, + callStatus, + callDuration, + initializeChat, + createNewChannel, + joinChannel, + sendMessage, + startCall, + endCall, + addMessage, + setCallDuration, + }; + + return {children}; +}; + +// Custom hook to use chat context +export const useChat = (): ChatContextType => { + const context = useContext(ChatContext); + if (!context) { + throw new Error('useChat must be used within ChatProvider'); + } + return context; +}; diff --git a/client/src/hooks/useAudioNotification.ts b/client/src/hooks/useAudioNotification.ts new file mode 100644 index 0000000..3821206 --- /dev/null +++ b/client/src/hooks/useAudioNotification.ts @@ -0,0 +1,14 @@ +/** + * Custom hook for audio notifications + */ + +import { useCallback } from 'react'; +import { playBeep } from '../utils/audioNotification'; + +export const useAudioNotification = () => { + const notify = useCallback(() => { + playBeep(); + }, []); + + return { notify }; +}; diff --git a/client/src/hooks/useCallTimer.ts b/client/src/hooks/useCallTimer.ts new file mode 100644 index 0000000..f163754 --- /dev/null +++ b/client/src/hooks/useCallTimer.ts @@ -0,0 +1,45 @@ +/** + * Custom hook for call state management + */ + +import { useState, useEffect, useCallback } from 'react'; + +export const useCallTimer = () => { + const [duration, setDuration] = useState(0); + const [isRunning, setIsRunning] = useState(false); + + const startTimer = useCallback(() => { + setIsRunning(true); + }, []); + + const stopTimer = useCallback(() => { + setIsRunning(false); + setDuration(0); + }, []); + + const formatDuration = useCallback((seconds: number): string => { + const m = Math.floor(seconds / 60) + .toString() + .padStart(2, '0'); + const s = (seconds % 60).toString().padStart(2, '0'); + return `${m}:${s}`; + }, []); + + useEffect(() => { + if (!isRunning) return; + + const interval = setInterval(() => { + setDuration((prev) => prev + 1); + }, 1000); + + return () => clearInterval(interval); + }, [isRunning]); + + return { + duration, + setDuration, + startTimer, + stopTimer, + formatDuration, + }; +}; diff --git a/client/src/hooks/useUrlHash.ts b/client/src/hooks/useUrlHash.ts new file mode 100644 index 0000000..782f6d7 --- /dev/null +++ b/client/src/hooks/useUrlHash.ts @@ -0,0 +1,29 @@ +/** + * Custom hook for URL hash management + */ + +import { useEffect, useState } from 'react'; +import { getUrlHash, updateUrlHash, hasValidHash } from '../utils/callTimer'; + +export const useUrlHash = () => { + const [hash, setHash] = useState(''); + + useEffect(() => { + const initialHash = getUrlHash(); + if (hasValidHash()) { + setHash(initialHash); + } + }, []); + + const updateHash = (newHash: string) => { + setHash(newHash); + updateUrlHash(newHash); + }; + + return { + hash, + setHash, + updateHash, + hasValidHash: hasValidHash(), + }; +}; diff --git a/client/src/main.tsx b/client/src/main.tsx new file mode 100644 index 0000000..e1a0102 --- /dev/null +++ b/client/src/main.tsx @@ -0,0 +1,17 @@ +/** + * Application entry point + */ + +import React from 'react'; +import ReactDOM from 'react-dom/client'; +import { ChatProvider } from './context/ChatContext'; +import App from './App'; + +const root = ReactDOM.createRoot(document.getElementById('app')!); +root.render( + + + + + +); diff --git a/client/src/styles/global.css b/client/src/styles/global.css new file mode 100644 index 0000000..5787ed4 --- /dev/null +++ b/client/src/styles/global.css @@ -0,0 +1,94 @@ +/** + * Global CSS variables + */ + +:root { + --primary: #6366f1; + --primary-hover: #4f46e5; + --bg: #0f172a; + --card-bg: rgba(30, 41, 59, 0.7); + --text: #f8fafc; + --text-muted: #94a3b8; + --danger: #ef4444; + --success: #10b981; + --glass: rgba(255, 255, 255, 0.05); + --glass-border: rgba(255, 255, 255, 0.1); +} + +* { + box-sizing: border-box; + margin: 0; + padding: 0; +} + +html, +body { + height: 100%; + width: 100%; +} + +body { + font-family: 'Outfit', sans-serif; + background-color: var(--bg); + color: var(--text); + height: 100vh; + height: 100dvh; + overflow: hidden; + background-image: radial-gradient(at 0% 0%, hsla(253, 16%, 7%, 1) 0, transparent 50%), + radial-gradient(at 50% 0%, hsla(225, 39%, 30%, 1) 0, transparent 50%), + radial-gradient(at 100% 0%, hsla(339, 49%, 30%, 1) 0, transparent 50%); +} + +#root, +#app { + height: 100%; + display: flex; + justify-content: center; + align-items: center; + padding: 20px; +} + +.glass { + background: var(--glass); + backdrop-filter: blur(12px); + -webkit-backdrop-filter: blur(12px); + border: 1px solid var(--glass-border); + box-shadow: 0 8px 32px 0 rgba(0, 0, 0, 0.37); +} + +.hidden { + display: none !important; +} + +/* Scrollbar styling */ +::-webkit-scrollbar { + width: 8px; +} + +::-webkit-scrollbar-track { + background: rgba(255, 255, 255, 0.05); +} + +::-webkit-scrollbar-thumb { + background: rgba(99, 102, 241, 0.4); + border-radius: 4px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(99, 102, 241, 0.6); +} + +/* Mobile responsive */ +@media (max-width: 480px) { + + #root, + #app { + padding: 0; + align-items: stretch; + } + + body { + height: 100vh; + height: 100dvh; + } +} \ No newline at end of file diff --git a/client/src/types/index.ts b/client/src/types/index.ts new file mode 100644 index 0000000..fce15d3 --- /dev/null +++ b/client/src/types/index.ts @@ -0,0 +1,100 @@ +/** + * Type definitions for Chat E2EE application + */ + +// Chat instance type from @chat-e2ee/service +export interface ChatInstance { + init: () => Promise; + getKeyPair: () => { privateKey: string; publicKey: string }; + getLink: () => Promise<{ hash: string }>; + setChannel: (hash: string, userId: string) => Promise; + getUsersInChannel: () => Promise; + startCall: () => Promise; + on: (event: string, callback: Function) => void; + encrypt: (data: { text: string }) => { send: () => Promise }; +} + +// Message type +export interface Message { + sender: string; + text: string; + type: 'sent' | 'received'; + timestamp: Date; +} + +// Call type +export interface Call { + on: (event: string, callback: Function) => void; + endCall: () => Promise; + state?: string; +} + +// Setup view states +export type SetupView = 'initial' | 'create' | 'join'; + +// Chat app state +export interface AppState { + chat: ChatInstance | null; + userId: string; + channelHash: string; + privateKey: string; + setupView: SetupView; + messages: Message[]; + isConnected: boolean; + callActive: boolean; +} + +// Chat context type +export interface ChatContextType { + // State + chat: ChatInstance | null; + userId: string; + channelHash: string; + privateKey: string; + messages: Message[]; + isConnected: boolean; + callActive: boolean; + callStatus: string; + callDuration: number; + + // Methods + initializeChat: () => Promise; + createNewChannel: () => Promise; + joinChannel: (hash: string) => Promise; + sendMessage: (text: string) => Promise; + startCall: () => Promise; + endCall: () => Promise; + addMessage: (message: Message) => void; + setCallDuration: (duration: number) => void; +} + +// Common component props +export interface ButtonProps { + variant?: 'primary' | 'secondary' | 'danger'; + size?: 'small' | 'large'; + onClick: () => void; + disabled?: boolean; + icon?: boolean; + circle?: boolean; + children: React.ReactNode; + className?: string; + title?: string; +} + +export interface InputProps { + label?: string; + placeholder?: string; + value: string; + onChange: (value: string) => void; + readOnly?: boolean; + type?: string; + className?: string; +} + +// Setup overlay props +export interface SetupOverlayProps { + setupView: SetupView; + onViewChange: (view: SetupView) => void; + onChannelJoin: (hash: string) => Promise; + status?: string; +} diff --git a/client/src/utils/audioNotification.ts b/client/src/utils/audioNotification.ts new file mode 100644 index 0000000..bcc3498 --- /dev/null +++ b/client/src/utils/audioNotification.ts @@ -0,0 +1,25 @@ +/** + * Audio notification utility - generates beep sound + */ + +export function playBeep() { + try { + const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext; + if (!AudioContextClass) return; + + const ctx = new AudioContextClass(); + const oscillator = ctx.createOscillator(); + const gainNode = ctx.createGain(); + + oscillator.connect(gainNode); + gainNode.connect(ctx.destination); + oscillator.type = 'sine'; + oscillator.frequency.setValueAtTime(880, ctx.currentTime); + gainNode.gain.setValueAtTime(0.3, ctx.currentTime); + gainNode.gain.linearRampToValueAtTime(0, ctx.currentTime + 0.4); + oscillator.start(ctx.currentTime); + oscillator.stop(ctx.currentTime + 0.4); + } catch (err) { + console.warn('Audio notification not available:', err); + } +} diff --git a/client/src/utils/callTimer.ts b/client/src/utils/callTimer.ts new file mode 100644 index 0000000..c31ef24 --- /dev/null +++ b/client/src/utils/callTimer.ts @@ -0,0 +1,27 @@ +/** + * URL hash handling utilities + */ + +/** + * Extract hash from current URL + */ +export function getUrlHash(): string { + return window.location.hash.replace('#', ''); +} + +/** + * Update URL with hash + */ +export function updateUrlHash(hash: string): void { + if (hash) { + window.location.hash = hash; + } +} + +/** + * Check if URL contains a valid hash + */ +export function hasValidHash(): boolean { + const hash = getUrlHash(); + return hash.length > 5; +} diff --git a/client/src/utils/messageHandling.ts b/client/src/utils/messageHandling.ts new file mode 100644 index 0000000..c385352 --- /dev/null +++ b/client/src/utils/messageHandling.ts @@ -0,0 +1,22 @@ +/** + * Message handling utilities + */ + +import { Message } from '../types/index'; + +export function createMessage( + sender: string, + text: string, + type: 'sent' | 'received' +): Message { + return { + sender, + text, + type, + timestamp: new Date(), + }; +} + +export function formatMessageTime(date: Date): string { + return date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); +} diff --git a/client/tsconfig.json b/client/tsconfig.json index 684eee8..6095412 100644 --- a/client/tsconfig.json +++ b/client/tsconfig.json @@ -9,6 +9,7 @@ "DOM.Iterable" ], "skipLibCheck": true, + "jsx": "react-jsx", /* Bundler mode */ "moduleResolution": "bundler", "allowImportingTsExtensions": true, @@ -23,6 +24,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 index c3e46f9..cd241b7 100644 --- a/client/vite.config.ts +++ b/client/vite.config.ts @@ -1,8 +1,10 @@ import { defineConfig, loadEnv } from 'vite'; +import react from '@vitejs/plugin-react'; export default defineConfig(({ mode }) => { const env = loadEnv(mode, process.cwd(), ''); return { + plugins: [react()], define: { 'process.env.CHATE2EE_API_URL': JSON.stringify(env.CHATE2EE_API_URL ?? ''), }, diff --git a/package-lock.json b/package-lock.json index 1b5d55b..449e999 100644 --- a/package-lock.json +++ b/package-lock.json @@ -51,9 +51,14 @@ "name": "chat-e2ee-client", "version": "1.0.0", "dependencies": { - "@chat-e2ee/service": "*" + "@chat-e2ee/service": "*", + "react": "^18.2.0", + "react-dom": "^18.2.0" }, "devDependencies": { + "@types/react": "^18.2.37", + "@types/react-dom": "^18.2.15", + "@vitejs/plugin-react": "^4.2.1", "typescript": "^5.6.2", "vite": "^5.4.1" } @@ -67,19 +72,6 @@ "node": ">=0.10.0" } }, - "node_modules/@ampproject/remapping": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.2.1.tgz", - "integrity": "sha512-lFMjJTrFL3j7L9yBxwYfCq2k6qqwHyzuUl/XBnif78PWTJYyL/dfowQHWE3sp6U6ZzqWiiIZnpTMO96zhkjwtg==", - "dev": true, - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.0", - "@jridgewell/trace-mapping": "^0.3.9" - }, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@babel/code-frame": { "version": "7.29.0", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", @@ -96,30 +88,30 @@ } }, "node_modules/@babel/compat-data": { - "version": "7.25.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.25.4.tgz", - "integrity": "sha512-+LGRog6RAsCJrrrg/IO6LGmpphNe5DiK30dGjCoxxeGv49B10/3XYGxPsAwrDlMFcFEvdAUavDT8r9k/hSyQqQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", "dev": true, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/core": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.25.2.tgz", - "integrity": "sha512-BBt3opiCOxUr9euZ5/ro/Xv8/V7yJ5bjYMqG/C1YAo8MIKAnumZalCN+msbci3Pigy4lIQfPUpfMM27HMGaYEA==", - "dev": true, - "dependencies": { - "@ampproject/remapping": "^2.2.0", - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.0", - "@babel/helper-compilation-targets": "^7.25.2", - "@babel/helper-module-transforms": "^7.25.2", - "@babel/helpers": "^7.25.0", - "@babel/parser": "^7.25.0", - "@babel/template": "^7.25.0", - "@babel/traverse": "^7.25.2", - "@babel/types": "^7.25.2", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", @@ -167,29 +159,30 @@ } }, "node_modules/@babel/generator": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.25.6.tgz", - "integrity": "sha512-VPC82gr1seXOpkjAAKoLhP50vx4vGNlF4msF64dSFq1P8RfB+QAuJWGHPXXPc8QyfVWwwB/TNNU4+ayZmHNbZw==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", "dev": true, "dependencies": { - "@babel/types": "^7.25.6", - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25", - "jsesc": "^2.5.1" + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@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.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.25.2.tgz", - "integrity": "sha512-U2U5LsSaZ7TAt3cfaymQ8WHh0pxvdHoEk6HVpaexxixjyEquMh0L0YNJNM6CTGKMXV1iksi0iZkGw4AcFkPaaw==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", "dev": true, "dependencies": { - "@babel/compat-data": "^7.25.2", - "@babel/helper-validator-option": "^7.24.8", - "browserslist": "^4.23.1", + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" }, @@ -206,29 +199,37 @@ "semver": "bin/semver.js" } }, + "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, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@babel/helper-module-imports": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.7.tgz", - "integrity": "sha512-8AyH3C+74cgCVVXow/myrynrAGv+nTVg5vKu2nZph9x7RcRwzmh0VFallJuFTZ9mx6u4eSdXZfcOzSqTUm0HCA==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", "dev": true, "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/helper-module-transforms": { - "version": "7.25.2", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.25.2.tgz", - "integrity": "sha512-BjyRAbix6j/wv83ftcVJmBt72QtHI56C7JXZoG2xATiLpmoC7dpd8WnkikExHDVPpi/3qCmO6WY1EaXOluiecQ==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", "dev": true, "dependencies": { - "@babel/helper-module-imports": "^7.24.7", - "@babel/helper-simple-access": "^7.24.7", - "@babel/helper-validator-identifier": "^7.24.7", - "@babel/traverse": "^7.25.2" + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" }, "engines": { "node": ">=6.9.0" @@ -238,23 +239,10 @@ } }, "node_modules/@babel/helper-plugin-utils": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.8.tgz", - "integrity": "sha512-FFWx5142D8h2Mgr/iPVGH5G7w6jDn4jUSpZTyDnQO0Yn7Ks2Kuz6Pci8H6MPCoUJegd/UZQ3tAvfLCxQSnWWwg==", - "dev": true, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-simple-access": { - "version": "7.24.7", - "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.7.tgz", - "integrity": "sha512-zBAIvbCMh5Ts+b86r/CjU+4XGYIs+R1j951gxI3KmmxBMhCg4oQMsv6ZXQ64XOm/cvzfU1FmoCyt6+owc5QMYg==", + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz", + "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==", "dev": true, - "dependencies": { - "@babel/traverse": "^7.24.7", - "@babel/types": "^7.24.7" - }, "engines": { "node": ">=6.9.0" } @@ -280,9 +268,9 @@ } }, "node_modules/@babel/helper-validator-option": { - "version": "7.24.8", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.24.8.tgz", - "integrity": "sha512-xb8t9tD1MHLungh/AIoWYN+gVHaB9kwlu8gffXGSt3FFEIT7RjS+xWbc2vUD1UTZdIpKj/ab3rdqJ7ufngyi2Q==", + "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, "engines": { "node": ">=6.9.0" @@ -495,6 +483,36 @@ "@babel/core": "^7.0.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, + "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, + "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.28.6", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", @@ -511,27 +529,27 @@ } }, "node_modules/@babel/traverse": { - "version": "7.25.6", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.25.6.tgz", - "integrity": "sha512-9Vrcx5ZW6UwK5tvqsj0nGpp/XzqthkT0dqIc9g1AdtygFToNtTF67XzYS//dm+SAK9cp3B9R4ZO/46p63SCjlQ==", + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", "dev": true, "dependencies": { - "@babel/code-frame": "^7.24.7", - "@babel/generator": "^7.25.6", - "@babel/parser": "^7.25.6", - "@babel/template": "^7.25.0", - "@babel/types": "^7.25.6", - "debug": "^4.3.1", - "globals": "^11.1.0" + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" }, "engines": { "node": ">=6.9.0" } }, "node_modules/@babel/traverse/node_modules/debug": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", - "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", "dev": true, "dependencies": { "ms": "^2.1.3" @@ -545,15 +563,6 @@ } } }, - "node_modules/@babel/traverse/node_modules/globals": { - "version": "11.12.0", - "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", - "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", - "dev": true, - "engines": { - "node": ">=4" - } - }, "node_modules/@babel/traverse/node_modules/ms": { "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", @@ -1960,17 +1969,23 @@ } }, "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz", - "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==", + "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, "dependencies": { - "@jridgewell/set-array": "^1.2.1", - "@jridgewell/sourcemap-codec": "^1.4.10", + "@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, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" - }, - "engines": { - "node": ">=6.0.0" } }, "node_modules/@jridgewell/resolve-uri": { @@ -1982,25 +1997,16 @@ "node": ">=6.0.0" } }, - "node_modules/@jridgewell/set-array": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz", - "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==", - "dev": true, - "engines": { - "node": ">=6.0.0" - } - }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.4.15", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.4.15.tgz", - "integrity": "sha512-eF2rxCRulEKXHTRiDrDy6erMYWqNw4LPdQ8UQA4huuxaQsVeRPFl2oM8oDGxMFhJUWZf9McpLtJasDDZb/Bpeg==", + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", "dev": true }, "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.25", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz", - "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==", + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", "dev": true, "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", @@ -2089,6 +2095,12 @@ "node": ">=18" } }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-beta.27", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-beta.27.tgz", + "integrity": "sha512-+d0F4MKMCbeVUJwG96uQ4SgAznZNSq93I3V+9NHA4OpvqG8mRCpGdKmK8l/dl02h2CCDHwW2FqilnTyDcAnqjA==", + "dev": true + }, "node_modules/@rollup/rollup-android-arm-eabi": { "version": "4.60.1", "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", @@ -2686,6 +2698,12 @@ "form-data": "^4.0.0" } }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "dev": true + }, "node_modules/@types/qs": { "version": "6.9.7", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.7.tgz", @@ -2698,6 +2716,25 @@ "integrity": "sha512-EEhsLsD6UsDM1yFhAvy0Cjr6VwmpMWqFBCb9w07wVugF7w9nfajxLuVmngTIpgS6svCnm6Vaw+MZhoDCKnOfsw==", "dev": true }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "dev": true, + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/react-dom": { + "version": "18.3.7", + "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz", + "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==", + "dev": true, + "peerDependencies": { + "@types/react": "^18.0.0" + } + }, "node_modules/@types/send": { "version": "0.17.1", "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.1.tgz", @@ -2921,6 +2958,26 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@vitejs/plugin-react": { + "version": "4.7.0", + "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-4.7.0.tgz", + "integrity": "sha512-gUu9hwfWvvEDBBmgtAowQCojwZmJ5mcLn3aufeCsitijs3+f2NsrPtlAWIR6OPiqljl96GVCUbLe0HyqIpVaoA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.28.0", + "@babel/plugin-transform-react-jsx-self": "^7.27.1", + "@babel/plugin-transform-react-jsx-source": "^7.27.1", + "@rolldown/pluginutils": "1.0.0-beta.27", + "@types/babel__core": "^7.20.5", + "react-refresh": "^0.17.0" + }, + "engines": { + "node": "^14.18.0 || >=16.0.0" + }, + "peerDependencies": { + "vite": "^4.2.0 || ^5.0.0 || ^6.0.0 || ^7.0.0" + } + }, "node_modules/abbrev": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", @@ -3186,6 +3243,18 @@ "node": "^4.5.0 || >= 5.9" } }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.16", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.16.tgz", + "integrity": "sha512-Lyf3aK28zpsD1yQMiiHD4RvVb6UdMoo8xzG2XzFIfR9luPzOpcBlAsT/qfB1XWS1bxWT+UtE4WmQgsp297FYOA==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3271,9 +3340,9 @@ } }, "node_modules/browserslist": { - "version": "4.23.3", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.3.tgz", - "integrity": "sha512-btwCFJVjI4YWDNfau8RhZ+B1Q/VLoUITrm3RlP6y1tYGWIOa+InuYiRGXUBXo8nA1qKmHMyLB/iVQg5TT4eFoA==", + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", "dev": true, "funding": [ { @@ -3290,10 +3359,11 @@ } ], "dependencies": { - "caniuse-lite": "^1.0.30001646", - "electron-to-chromium": "^1.5.4", - "node-releases": "^2.0.18", - "update-browserslist-db": "^1.1.0" + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" }, "bin": { "browserslist": "cli.js" @@ -3394,9 +3464,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001663", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001663.tgz", - "integrity": "sha512-o9C3X27GLKbLeTYZ6HBOLU1tsAcBZsLis28wrVzddShCS16RujjHp9GDHKZqrB3meE0YjhawvMFsGb/igqiPzA==", + "version": "1.0.30001786", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001786.tgz", + "integrity": "sha512-4oxTZEvqmLLrERwxO76yfKM7acZo310U+v4kqexI2TL1DkkUEMT8UijrxxcnVdxR3qkVf5awGRX+4Z6aPHVKrA==", "dev": true, "funding": [ { @@ -3928,6 +3998,12 @@ "node": ">= 8" } }, + "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 + }, "node_modules/dargs": { "version": "8.1.0", "resolved": "https://registry.npmjs.org/dargs/-/dargs-8.1.0.tgz", @@ -4098,9 +4174,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.28", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.28.tgz", - "integrity": "sha512-VufdJl+rzaKZoYVUijN13QcXVF5dWPZANeFTLNy+OSpHdDL5ynXTF35+60RSBbaQYB1ae723lQXHCrf4pyLsMw==", + "version": "1.5.332", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.332.tgz", + "integrity": "sha512-7OOtytmh/rINMLwaFTbcMVvYXO3AUm029X0LcyfYk0B557RlPkdpTpnH9+htMlfu5dKwOmT0+Zs2Aw+lnn6TeQ==", "dev": true }, "node_modules/emittery": { @@ -6357,7 +6433,6 @@ "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/js-yaml": { @@ -6374,15 +6449,15 @@ } }, "node_modules/jsesc": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", - "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", "dev": true, "bin": { "jsesc": "bin/jsesc" }, "engines": { - "node": ">=4" + "node": ">=6" } }, "node_modules/json-buffer": { @@ -7041,6 +7116,17 @@ "url": "https://github.com/chalk/wrap-ansi?sponsor=1" } }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, "node_modules/lru-cache": { "version": "5.1.1", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", @@ -7369,9 +7455,9 @@ "dev": true }, "node_modules/node-releases": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.18.tgz", - "integrity": "sha512-d9VeXT4SJ7ZeOqGX6R5EM022wpL+eWPooLI+5UpWn2jCT1aosUQEhQP214x33Wkwx3JQMvIm+tIoVOdodFS40g==", + "version": "2.0.37", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.37.tgz", + "integrity": "sha512-1h5gKZCF+pO/o3Iqt5Jp7wc9rH3eJJ0+nh/CIoiRwjRxde/hAHyLPXYN4V3CqKAbiZPSeJFSWHmJsbkicta0Eg==", "dev": true }, "node_modules/nodemon": { @@ -7978,6 +8064,38 @@ "node": ">= 0.8" } }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", + "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.2" + }, + "peerDependencies": { + "react": "^18.3.1" + } + }, + "node_modules/react-refresh": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.17.0.tgz", + "integrity": "sha512-z6F7K9bV85EfseRCp2bzrpyQ0Gkw1uLoCel9XBVWPg/TjRj94SkJzUTGfOa4bs7iJvBWtQG0Wq7wnI0syw3EBQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/readdirp": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", @@ -8220,6 +8338,14 @@ "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", "license": "MIT" }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, "node_modules/semver": { "version": "7.6.3", "resolved": "https://registry.npmjs.org/semver/-/semver-7.6.3.tgz", @@ -9167,9 +9293,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.0.tgz", - "integrity": "sha512-EdRAaAyk2cUE1wOf2DkEhzxqOQvFOoRJFNS6NeyJ01Gp2beMRpBAINjM2iDXE3KCuKhwnvHIQCJm6ThL2Z+HzQ==", + "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": [ { @@ -9186,8 +9312,8 @@ } ], "dependencies": { - "escalade": "^3.1.2", - "picocolors": "^1.0.1" + "escalade": "^3.2.0", + "picocolors": "^1.1.1" }, "bin": { "update-browserslist-db": "cli.js" From bbd10fcd1dd1258f3f82fc071cd07047711419d2 Mon Sep 17 00:00:00 2001 From: VishnuGadekar7 Date: Wed, 8 Apr 2026 22:30:46 +0530 Subject: [PATCH 2/2] github workflow issues resolved --- client/src/App.tsx | 4 +- .../components/CallOverlay/CallOverlay.tsx | 6 ++- .../ChatContainer/ChatContainer.tsx | 2 +- .../components/ChatContainer/ChatFooter.tsx | 2 +- .../SetupOverlay/CreateHashView.tsx | 2 - .../components/SetupOverlay/SetupOverlay.tsx | 21 +++++----- client/src/context/ChatContext.tsx | 38 ++++++++++++------- client/src/hooks/useUrlHash.ts | 2 +- client/src/types/index.ts | 31 +++------------ client/src/utils/urlHash.ts | 27 +++++++++++++ client/tsconfig.json | 3 +- docker/Dockerfile | 6 +-- 12 files changed, 79 insertions(+), 65 deletions(-) create mode 100644 client/src/utils/urlHash.ts diff --git a/client/src/App.tsx b/client/src/App.tsx index e6032ae..13b00b9 100644 --- a/client/src/App.tsx +++ b/client/src/App.tsx @@ -6,11 +6,11 @@ import React, { useEffect, useState } from 'react'; import { useChat } from './context/ChatContext'; import { SetupOverlay } from './components/SetupOverlay/SetupOverlay'; import { ChatContainer } from './components/ChatContainer/ChatContainer'; -import { updateUrlHash } from './utils/callTimer'; +import { updateUrlHash } from './utils/urlHash'; import './styles/global.css'; const AppContent: React.FC = () => { - const { initializeChat, joinChannel, channelHash } = useChat(); + const { initializeChat, joinChannel } = useChat(); const [showSetup, setShowSetup] = useState(true); const [error, setError] = useState(''); diff --git a/client/src/components/CallOverlay/CallOverlay.tsx b/client/src/components/CallOverlay/CallOverlay.tsx index 8de3cbd..a72a066 100644 --- a/client/src/components/CallOverlay/CallOverlay.tsx +++ b/client/src/components/CallOverlay/CallOverlay.tsx @@ -11,13 +11,15 @@ import './CallOverlay.css'; export const CallOverlay: React.FC = () => { const { callActive, callStatus, endCall } = useChat(); - const { duration, formatDuration, startTimer } = useCallTimer(); + const { duration, formatDuration, startTimer, stopTimer } = useCallTimer(); useEffect(() => { if (callActive && callStatus === 'Connected') { startTimer(); + } else { + stopTimer(); } - }, [callActive, callStatus, startTimer]); + }, [callActive, callStatus, startTimer, stopTimer]); if (!callActive) return null; diff --git a/client/src/components/ChatContainer/ChatContainer.tsx b/client/src/components/ChatContainer/ChatContainer.tsx index c6d7757..5c77c70 100644 --- a/client/src/components/ChatContainer/ChatContainer.tsx +++ b/client/src/components/ChatContainer/ChatContainer.tsx @@ -16,7 +16,7 @@ interface ChatContainerProps { export const ChatContainer: React.FC = ({ isHidden }) => { const { startCall } = useChat(); - const [isStartingCall, setIsStartingCall] = useState(false); + const [, setIsStartingCall] = useState(false); const handleStartCall = async () => { try { diff --git a/client/src/components/ChatContainer/ChatFooter.tsx b/client/src/components/ChatContainer/ChatFooter.tsx index 1ea9e83..59ea152 100644 --- a/client/src/components/ChatContainer/ChatFooter.tsx +++ b/client/src/components/ChatContainer/ChatFooter.tsx @@ -47,7 +47,7 @@ export const ChatFooter: React.FC = () => { placeholder="Type a secure message..." value={message} onChange={(e) => setMessage(e.target.value)} - onKeyPress={handleKeyPress} + onKeyDown={handleKeyPress} disabled={isSending} />