Skip to content

Commit 77d0603

Browse files
feat: add the chat
Co-authored-by: Yurii Kinakh <yuriikinakh5@gmail.com>
1 parent fc3a250 commit 77d0603

File tree

2 files changed

+201
-3
lines changed

2 files changed

+201
-3
lines changed

src/App.css

Lines changed: 118 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -85,7 +85,7 @@
8585
.vp-connecting-overlay {
8686
position: absolute !important;
8787
top: 64px !important;
88-
bottom: 136px !important;
88+
bottom: 64px !important;
8989
left: 0 !important;
9090
right: 0 !important;
9191
z-index: 9 !important;
@@ -283,7 +283,7 @@
283283
.vp-live-status-bar {
284284
position: absolute !important;
285285
left: 50% !important;
286-
bottom: 40px !important;
286+
bottom: 76px !important;
287287
transform: translateX(-50%) !important;
288288
z-index: 11 !important;
289289
width: min(320px, calc(100% - 32px)) !important;
@@ -328,6 +328,122 @@
328328
line-height: 1 !important;
329329
}
330330

331+
.vp-chat-overlay {
332+
position: absolute !important;
333+
top: 64px !important;
334+
bottom: 56px !important;
335+
left: 0 !important;
336+
right: 48px !important;
337+
z-index: 9 !important;
338+
display: flex !important;
339+
flex-direction: column !important;
340+
overflow: hidden !important;
341+
pointer-events: none !important;
342+
}
343+
344+
.vp-chat-messages {
345+
display: flex !important;
346+
flex-direction: column !important;
347+
gap: 8px !important;
348+
overflow-y: auto !important;
349+
padding: 0 16px !important;
350+
max-height: 100% !important;
351+
margin-top: auto !important;
352+
pointer-events: auto !important;
353+
scrollbar-width: none !important;
354+
}
355+
356+
.vp-chat-messages::-webkit-scrollbar {
357+
display: none !important;
358+
}
359+
360+
.vp-chat-message {
361+
display: flex !important;
362+
flex-direction: row !important;
363+
align-items: flex-start !important;
364+
gap: 8px !important;
365+
}
366+
367+
.vp-chat-avatar {
368+
width: 32px !important;
369+
height: 32px !important;
370+
border-radius: 9999px !important;
371+
object-fit: cover !important;
372+
flex-shrink: 0 !important;
373+
border: 1.5px solid rgba(255, 255, 255, 0.35) !important;
374+
}
375+
376+
.vp-chat-content {
377+
display: flex !important;
378+
flex-direction: column !important;
379+
gap: 2px !important;
380+
}
381+
382+
.vp-chat-username {
383+
color: rgba(255, 255, 255, 0.85) !important;
384+
font-size: 14px !important;
385+
line-height: 1 !important;
386+
font-weight: bold !important;
387+
}
388+
389+
.vp-chat-text {
390+
color: #ffffff !important;
391+
font-size: 16px !important;
392+
line-height: 1 !important;
393+
word-break: break-word !important;
394+
}
395+
396+
.vp-chat-input-bar {
397+
position: absolute !important;
398+
bottom: 0 !important;
399+
left: 0 !important;
400+
right: 0 !important;
401+
z-index: 9 !important;
402+
height: 56px !important;
403+
display: flex !important;
404+
align-items: center !important;
405+
padding: 0 16px !important;
406+
}
407+
408+
.vp-chat-input-container {
409+
flex: 1 !important;
410+
display: flex !important;
411+
flex-direction: row !important;
412+
align-items: center !important;
413+
height: 40px !important;
414+
border: 1px solid #ffffff !important;
415+
border-radius: 9999px !important;
416+
padding: 0 6px 0 16px !important;
417+
}
418+
419+
.vp-chat-input {
420+
flex: 1 !important;
421+
height: 100% !important;
422+
background: transparent !important;
423+
border: none !important;
424+
color: #ffffff !important;
425+
font-size: 14px !important;
426+
padding: 0 !important;
427+
outline: none !important;
428+
}
429+
430+
.vp-chat-input::placeholder {
431+
color: rgba(255, 255, 255, 0.8) !important;
432+
}
433+
434+
.vp-chat-send-btn {
435+
width: 32px !important;
436+
height: 32px !important;
437+
flex-shrink: 0 !important;
438+
display: flex !important;
439+
align-items: center !important;
440+
justify-content: center !important;
441+
border-radius: 9999px !important;
442+
background: transparent !important;
443+
border: none !important;
444+
cursor: pointer !important;
445+
}
446+
331447
@media (min-width: 768px) {
332448
.vp-room {
333449
bottom: 20px !important;

src/components/RoomScreen.js

Lines changed: 83 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,11 @@ import {
66
VideoTrack,
77
useLocalParticipant,
88
useRemoteParticipants,
9+
useRoomContext,
910
useTracks,
1011
} from '@livekit/components-react';
1112
import { Track, AudioPresets, RoomEvent, VideoQuality } from 'livekit-client';
12-
import { AudioLines, X } from 'lucide-react';
13+
import { AudioLines, X, SendHorizonal } from 'lucide-react';
1314
import { api } from '../utils/api';
1415
import { AVATAR_URL, LIVEKIT_URL, USERNAME } from '../utils/constants';
1516

@@ -56,6 +57,44 @@ function useMediaControls() {
5657
};
5758
}
5859

60+
function useChat() {
61+
const room = useRoomContext();
62+
const [messages, setMessages] = useState([]);
63+
const [input, setInput] = useState('');
64+
65+
useEffect(() => {
66+
const handleData = (payload) => {
67+
try {
68+
const decoder = new TextDecoder();
69+
const json = JSON.parse(decoder.decode(payload));
70+
if (json.message) {
71+
setMessages((prev) => [...prev, json]);
72+
}
73+
} catch (e) {
74+
console.error('Failed to parse chat message', e);
75+
}
76+
};
77+
room.on(RoomEvent.DataReceived, handleData);
78+
return () => room.off(RoomEvent.DataReceived, handleData);
79+
}, [room]);
80+
81+
const sendMessage = useCallback(() => {
82+
const text = input.trim();
83+
if (!text) return;
84+
const msg = { username: USERNAME, avatar: 'default.jpg', message: text };
85+
try {
86+
const encoder = new TextEncoder();
87+
room.localParticipant.publishData(encoder.encode(JSON.stringify(msg)), { reliable: true });
88+
} catch (e) {
89+
console.error('Failed to send chat message', e);
90+
}
91+
setMessages((prev) => [...prev, msg]);
92+
setInput('');
93+
}, [input, room]);
94+
95+
return { messages, input, setInput, sendMessage };
96+
}
97+
5998
function ParticipantTile({ participant }) {
6099
const tracks = useTracks(
61100
[
@@ -98,6 +137,12 @@ function ParticipantTile({ participant }) {
98137

99138
function RoomContainer({ roomName, onLeave }) {
100139
const { isMuted, enableMic, disableMic } = useMediaControls();
140+
const { messages, input, setInput, sendMessage } = useChat();
141+
const messagesEndRef = useRef(null);
142+
143+
useEffect(() => {
144+
messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' });
145+
}, [messages]);
101146

102147
const hadRemoteParticipantRef = useRef(false);
103148

@@ -202,6 +247,43 @@ function RoomContainer({ roomName, onLeave }) {
202247
) : null}
203248
<span className="vp-live-status-subtitle">Your camera stays off</span>
204249
</div>
250+
251+
<div className="vp-chat-overlay">
252+
<div className="vp-chat-messages">
253+
{messages.map((msg, i) => (
254+
<div key={i} className="vp-chat-message">
255+
<img
256+
src={AVATAR_URL + (msg.avatar || 'default.jpg')}
257+
alt={msg.username}
258+
className="vp-chat-avatar"
259+
crossOrigin="anonymous"
260+
onError={(e) => { e.currentTarget.src = AVATAR_URL + 'default.jpg'; }}
261+
/>
262+
<div className="vp-chat-content">
263+
<span className="vp-chat-username">{msg.username}</span>
264+
<span className="vp-chat-text">{msg.message}</span>
265+
</div>
266+
</div>
267+
))}
268+
<div ref={messagesEndRef} />
269+
</div>
270+
</div>
271+
272+
{remoteParticipants.length > 0 && <div className="vp-chat-input-bar">
273+
<div className="vp-chat-input-container">
274+
<input
275+
className="vp-chat-input"
276+
type="text"
277+
placeholder="Type a message..."
278+
value={input}
279+
onChange={(e) => setInput(e.target.value)}
280+
onKeyDown={(e) => { if (e.key === 'Enter') sendMessage(); }}
281+
/>
282+
<button className="vp-chat-send-btn" onClick={sendMessage} aria-label="Send">
283+
<SendHorizonal size={20} color="white" />
284+
</button>
285+
</div>
286+
</div>}
205287
</div>
206288
);
207289
}

0 commit comments

Comments
 (0)