Skip to content

Commit eb0ad68

Browse files
authored
Merge pull request #9 from Arhaan-Siddiquee/master
[Feat: Added Chat Feature]
2 parents 3d7447a + bb0b5de commit eb0ad68

File tree

1 file changed

+286
-0
lines changed

1 file changed

+286
-0
lines changed

src/pages/App.jsx

Lines changed: 286 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,11 +54,14 @@ export default function App() {
5454
const [connected, setConnected] = useState(false)
5555
const [streamActive, setStreamActive] = useState(false)
5656
const [showQR, setShowQR] = useState(false)
57+
const [messages, setMessages] = useState([])
58+
const [msgInput, setMsgInput] = useState('')
5759

5860
const peerRef = useRef(null)
5961
const connRef = useRef(null)
6062
const mediaRef = useRef(null)
6163
const audioRef = useRef(null)
64+
const chatEndRef = useRef(null)
6265

6366
const peerOptions = useMemo(() => ({
6467
host: cfg.host, port: Number(cfg.port), secure: !!cfg.secure, path: cfg.path || '/',
@@ -110,6 +113,10 @@ export default function App() {
110113
}
111114
}, [peerOptions])
112115

116+
useEffect(() => {
117+
chatEndRef.current?.scrollIntoView({ behavior: 'smooth' })
118+
}, [messages])
119+
113120
function pushLog(x) { setLog((l) => [x, ...l].slice(0, 200)) }
114121

115122
async function getMic() {
@@ -138,6 +145,14 @@ export default function App() {
138145
conn.on('data', (data) => {
139146
if (data?.type === 'presence') {
140147
setPeers((p) => mergePeers(p, data.payload))
148+
} else if (data?.type === 'message') {
149+
setMessages((msgs) => [...msgs, {
150+
text: data.text,
151+
sender: data.sender,
152+
timestamp: data.timestamp,
153+
isMe: false
154+
}])
155+
pushLog(`Message from ${data.sender}: ${data.text}`)
141156
} else if (data?.type === 'signal') {
142157
// Place for extra messages if needed
143158
}
@@ -195,12 +210,245 @@ export default function App() {
195210
}
196211
}
197212

213+
function sendMessage() {
214+
if (!msgInput.trim() || !connRef.current?.open) return
215+
216+
const message = {
217+
type: 'message',
218+
text: msgInput,
219+
sender: label || 'Anonymous',
220+
timestamp: Date.now()
221+
}
222+
223+
connRef.current.send(message)
224+
225+
setMessages((msgs) => [...msgs, {
226+
text: msgInput,
227+
sender: label || 'Anonymous',
228+
timestamp: Date.now(),
229+
isMe: true
230+
}])
231+
232+
pushLog(`You sent: ${msgInput}`)
233+
setMsgInput('')
234+
}
235+
236+
function handleKeyPress(e) {
237+
if (e.key === 'Enter' && !e.shiftKey) {
238+
e.preventDefault()
239+
sendMessage()
240+
}
241+
}
242+
198243
const connectDisabled = !peerIdInput || status !== 'ready'
199244
const shareableUrl = myId ? `${window.location.origin}${window.location.pathname}?peer=${myId}` : ''
200245
const qrUrl = myId ? generateQR(shareableUrl) : ''
201246

202247
return (
203248
<div className="app">
249+
<style>{`
250+
.app {
251+
min-height: 100vh;
252+
background: #0f172a;
253+
color: #e2e8f0;
254+
padding: 20px;
255+
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
256+
}
257+
.card {
258+
background: #1e293b;
259+
border-radius: 12px;
260+
padding: 20px;
261+
max-width: 1200px;
262+
margin: 0 auto;
263+
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.3);
264+
}
265+
.header {
266+
display: flex;
267+
align-items: center;
268+
gap: 12px;
269+
flex-wrap: wrap;
270+
}
271+
.h {
272+
margin: 0;
273+
font-size: 24px;
274+
font-weight: 600;
275+
color: #f1f5f9;
276+
}
277+
.badge {
278+
background: #334155;
279+
padding: 6px 12px;
280+
border-radius: 6px;
281+
font-size: 13px;
282+
font-weight: 500;
283+
}
284+
.badge.small {
285+
font-size: 11px;
286+
padding: 4px 8px;
287+
}
288+
.row {
289+
display: flex;
290+
gap: 10px;
291+
align-items: center;
292+
}
293+
.grow {
294+
flex: 1;
295+
}
296+
.small {
297+
font-size: 13px;
298+
color: #94a3b8;
299+
margin-bottom: 4px;
300+
}
301+
.mono {
302+
font-family: 'Courier New', monospace;
303+
font-size: 13px;
304+
background: #0f172a;
305+
padding: 8px;
306+
border-radius: 6px;
307+
margin-top: 4px;
308+
}
309+
button {
310+
background: #3b82f6;
311+
color: white;
312+
border: none;
313+
padding: 10px 16px;
314+
border-radius: 6px;
315+
cursor: pointer;
316+
font-size: 14px;
317+
font-weight: 500;
318+
transition: all 0.2s;
319+
}
320+
button:hover:not(:disabled) {
321+
background: #2563eb;
322+
transform: translateY(-1px);
323+
}
324+
button:disabled {
325+
opacity: 0.5;
326+
cursor: not-allowed;
327+
}
328+
button.secondary {
329+
background: #475569;
330+
}
331+
button.secondary:hover:not(:disabled) {
332+
background: #334155;
333+
}
334+
button.primary {
335+
background: #10b981;
336+
}
337+
button.primary:hover:not(:disabled) {
338+
background: #059669;
339+
}
340+
button.danger {
341+
background: #ef4444;
342+
}
343+
button.danger:hover:not(:disabled) {
344+
background: #dc2626;
345+
}
346+
input {
347+
background: #0f172a;
348+
border: 1px solid #334155;
349+
color: #e2e8f0;
350+
padding: 10px;
351+
border-radius: 6px;
352+
font-size: 14px;
353+
width: 100%;
354+
}
355+
input:focus {
356+
outline: none;
357+
border-color: #3b82f6;
358+
}
359+
.grid {
360+
display: grid;
361+
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
362+
gap: 16px;
363+
}
364+
.list {
365+
max-height: 200px;
366+
overflow-y: auto;
367+
background: #0f172a;
368+
padding: 12px;
369+
border-radius: 6px;
370+
}
371+
.list::-webkit-scrollbar {
372+
width: 6px;
373+
}
374+
.list::-webkit-scrollbar-track {
375+
background: #1e293b;
376+
}
377+
.list::-webkit-scrollbar-thumb {
378+
background: #475569;
379+
border-radius: 3px;
380+
}
381+
ul {
382+
margin: 8px 0;
383+
padding-left: 20px;
384+
}
385+
li {
386+
margin: 4px 0;
387+
}
388+
.chat-container {
389+
background: #0f172a;
390+
border-radius: 8px;
391+
padding: 16px;
392+
height: 400px;
393+
display: flex;
394+
flex-direction: column;
395+
}
396+
.chat-messages {
397+
flex: 1;
398+
overflow-y: auto;
399+
margin-bottom: 12px;
400+
display: flex;
401+
flex-direction: column;
402+
gap: 8px;
403+
}
404+
.chat-messages::-webkit-scrollbar {
405+
width: 6px;
406+
}
407+
.chat-messages::-webkit-scrollbar-track {
408+
background: #1e293b;
409+
}
410+
.chat-messages::-webkit-scrollbar-thumb {
411+
background: #475569;
412+
border-radius: 3px;
413+
}
414+
.message {
415+
padding: 8px 12px;
416+
border-radius: 8px;
417+
max-width: 70%;
418+
word-wrap: break-word;
419+
}
420+
.message.me {
421+
background: #3b82f6;
422+
align-self: flex-end;
423+
margin-left: auto;
424+
}
425+
.message.other {
426+
background: #334155;
427+
align-self: flex-start;
428+
}
429+
.message-sender {
430+
font-size: 11px;
431+
font-weight: 600;
432+
margin-bottom: 2px;
433+
opacity: 0.8;
434+
}
435+
.message-text {
436+
font-size: 14px;
437+
}
438+
.message-time {
439+
font-size: 10px;
440+
opacity: 0.6;
441+
margin-top: 2px;
442+
}
443+
.chat-input-container {
444+
display: flex;
445+
gap: 8px;
446+
}
447+
.chat-input {
448+
flex: 1;
449+
}
450+
`}</style>
451+
204452
<div className="card">
205453
<div className="header">
206454
<h2 className="h">Local P2P Voice Chat</h2>
@@ -261,6 +509,43 @@ export default function App() {
261509
</div>
262510
</div>
263511

512+
<div className="card" style={{ marginTop: 16, padding: 16 }}>
513+
<div className="small" style={{ marginBottom: 8 }}>Text Chat</div>
514+
<div className="chat-container">
515+
<div className="chat-messages">
516+
{messages.length === 0 ? (
517+
<div className="small" style={{ textAlign: 'center', opacity: 0.5, marginTop: 'auto', marginBottom: 'auto' }}>
518+
No messages yet. Connect with a peer to start chatting!
519+
</div>
520+
) : (
521+
messages.map((msg, i) => (
522+
<div key={i} className={`message ${msg.isMe ? 'me' : 'other'}`}>
523+
<div className="message-sender">{msg.sender}</div>
524+
<div className="message-text">{msg.text}</div>
525+
<div className="message-time">
526+
{new Date(msg.timestamp).toLocaleTimeString()}
527+
</div>
528+
</div>
529+
))
530+
)}
531+
<div ref={chatEndRef} />
532+
</div>
533+
<div className="chat-input-container">
534+
<input
535+
className="chat-input"
536+
placeholder={connected ? "Type a message..." : "Connect to a peer first"}
537+
value={msgInput}
538+
onChange={(e) => setMsgInput(e.target.value)}
539+
onKeyPress={handleKeyPress}
540+
disabled={!connected}
541+
/>
542+
<button onClick={sendMessage} disabled={!connected || !msgInput.trim()} className="primary">
543+
Send
544+
</button>
545+
</div>
546+
</div>
547+
</div>
548+
264549
<div className="card" style={{ marginTop: 16, padding: 16 }}>
265550
<audio ref={audioRef} autoPlay playsInline />
266551
</div>
@@ -281,6 +566,7 @@ export default function App() {
281566
<li>Audio is sent end-to-end via WebRTC. Without your own TURN, very strict NATs may block audio.</li>
282567
<li>Over GitHub Pages, only static hosting is available; we use PeerJS public broker for signalling.</li>
283568
<li>Use the QR code to quickly share your Peer ID with others on the same Wi-Fi.</li>
569+
<li>Text chat works independently of voice calls - you can chat without calling.</li>
284570
</ul>
285571
</div>
286572
</div>

0 commit comments

Comments
 (0)