-
-
Notifications
You must be signed in to change notification settings - Fork 1.7k
Implemented the who is typing feature #155
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
This file was deleted.
This file was deleted.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1 @@ | ||
| REACT_APP_API_URL=http://localhost:5000 | ||
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,68 +1,89 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useState, useEffect } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import queryString from 'query-string'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import queryString from "query-string"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import io from "socket.io-client"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import TextContainer from '../TextContainer/TextContainer'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Messages from '../Messages/Messages'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import InfoBar from '../InfoBar/InfoBar'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Input from '../Input/Input'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import TextContainer from "../TextContainer/TextContainer"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Messages from "../Messages/Messages"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import InfoBar from "../InfoBar/InfoBar"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import Input from "../Input/Input"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import './Chat.css'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ENDPOINT = 'https://project-chat-application.herokuapp.com/'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import "./Chat.css"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const ENDPOINT = "http://localhost:5000"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| let socket; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Chat = ({ location }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [name, setName] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [room, setRoom] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [users, setUsers] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [message, setMessage] = useState(''); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [name, setName] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [room, setRoom] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [users, setUsers] = useState([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [message, setMessage] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [messages, setMessages] = useState([]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const [typing, setTyping] = useState(""); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const { name, room } = queryString.parse(location.search); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket = io(ENDPOINT); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setRoom(room); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setName(name) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setName(name); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit('join', { name, room }, (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if(error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit("join", { name, room }, (error) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| alert(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [ENDPOINT, location.search]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, [location.search]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
23
to
+35
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Disconnect the socket on cleanup to prevent ghost users and leaked connections. When the component unmounts or the room changes (location.search), the socket remains connected. The server won’t receive a disconnect, leaving “stuck” users in roomData and continuing to broadcast typing/messages to an unmounted UI. useEffect(() => {
const { name, room } = queryString.parse(location.search);
socket = io(ENDPOINT);
setRoom(room);
setName(name);
socket.emit("join", { name, room }, (error) => {
if (error) {
alert(error);
}
});
- }, [location.search]);
+ return () => {
+ try {
+ socket?.emit("stopTyping");
+ } catch (_) {}
+ // Ensure the server gets a proper disconnect and the client frees resources.
+ socket?.disconnect();
+ };
+ }, [location.search]);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| useEffect(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on('message', message => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setMessages(messages => [ ...messages, message ]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on("message", (message) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setMessages((msgs) => [...msgs, message]); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on("roomData", ({ users }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setUsers(users); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // ✅ Handle typing events | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on("showTyping", (msg) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setTyping(msg); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.off("message"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.off("roomData"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.off("showTyping"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, []); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
37
to
+56
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Re-subscribe listeners on room change; avoid stale socket references. This effect uses an empty dependency array, so it won’t re-run if a new socket is created on a room change. That leaves the new socket without listeners, breaking message updates and typing indicators. Also, pass explicit handler refs to off(). - useEffect(() => {
- socket.on("message", (message) => {
- setMessages((msgs) => [...msgs, message]);
- });
-
- socket.on("roomData", ({ users }) => {
- setUsers(users);
- });
-
- // ✅ Handle typing events
- socket.on("showTyping", (msg) => {
- setTyping(msg);
- });
-
- return () => {
- socket.off("message");
- socket.off("roomData");
- socket.off("showTyping");
- };
- }, []);
+ useEffect(() => {
+ if (!socket) return;
+
+ const onMessage = (message) => setMessages((msgs) => [...msgs, message]);
+ const onRoomData = ({ users }) => setUsers(users);
+ const onShowTyping = (msg) => setTyping(msg);
+
+ socket.on("message", onMessage);
+ socket.on("roomData", onRoomData);
+ socket.on("showTyping", onShowTyping);
+
+ return () => {
+ socket.off("message", onMessage);
+ socket.off("roomData", onRoomData);
+ socket.off("showTyping", onShowTyping);
+ };
+ }, [location.search]);📝 Committable suggestion
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const sendMessage = (event) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| event.preventDefault(); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if(message) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit('sendMessage', message, () => setMessage('')); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (message) { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit("sendMessage", message, () => setMessage("")); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit("stopTyping"); // stop typing after sending | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
58
to
+65
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Guard against undefined socket and trim messages. On fast interactions, sendMessage can run before the socket is ready; also avoid sending whitespace messages. const sendMessage = (event) => {
event.preventDefault();
- if (message) {
- socket.emit("sendMessage", message, () => setMessage(""));
- socket.emit("stopTyping"); // stop typing after sending
- }
+ if (!socket) return;
+ const text = message.trim();
+ if (text) {
+ socket.emit("sendMessage", text, () => setMessage(""));
+ socket.emit("stopTyping"); // stop typing after sending
+ }
};📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="outerContainer"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <div className="container"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <InfoBar room={room} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Messages messages={messages} name={name} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Input message={message} setMessage={setMessage} sendMessage={sendMessage} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <InfoBar room={room} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Messages messages={messages} name={name} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {/* 👇 Typing indicator */} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| {typing && <p className="typing">{typing}</p>} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <Input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| message={message} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setMessage={setMessage} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| sendMessage={sendMessage} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket={socket} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+76
to
+81
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Render Input only when socket is ready to avoid runtime errors in Input. Input emits typing on change; if socket is not yet initialized, it can throw. Render-gate on socket readiness. - <Input
- message={message}
- setMessage={setMessage}
- sendMessage={sendMessage}
- socket={socket}
- />
+ {socket && (
+ <Input
+ message={message}
+ setMessage={setMessage}
+ sendMessage={sendMessage}
+ socket={socket}
+ />
+ )}Optional: Alternatively, add a null-check inside Input’s handleChange before emitting. 📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <TextContainer users={users}/> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <TextContainer users={users} /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </div> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default Chat; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -1,19 +1,40 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React from 'react'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import './Input.css'; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Input = ({ setMessage, sendMessage, message }) => ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form className="form"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="input" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="Type a message..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={message} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={({ target: { value } }) => setMessage(value)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onKeyPress={event => event.key === 'Enter' ? sendMessage(event) : null} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button className="sendButton" onClick={e => sendMessage(e)}>Send</button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default Input; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import React, { useEffect, useRef } from "react"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| import "./Input.css"; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const Input = ({ message, setMessage, sendMessage, socket }) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const typingTimeoutRef = useRef(null); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| const handleChange = (e) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| setMessage(e.target.value); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Emit typing event | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit("typing"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // Clear old timeout | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| // After 1.5s of no typing, send stopTyping | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| typingTimeoutRef.current = setTimeout(() => { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit("stopTyping"); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }, 1500); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+8
to
+21
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard socket emits and add a cleanup for the typing timer. Avoid exceptions when socket is null/not connected, and clear the timer on unmount to prevent orphaned timeouts. const handleChange = (e) => {
setMessage(e.target.value);
// Emit typing event
- socket.emit("typing");
+ if (socket) socket.emit("typing");
// Clear old timeout
if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current);
// After 1.5s of no typing, send stopTyping
typingTimeoutRef.current = setTimeout(() => {
- socket.emit("stopTyping");
+ if (socket) socket.emit("stopTyping");
}, 1500);
};
+// Optional: clear timer on unmount
+// React 16.9 supports this pattern
+// useEffect(() => () => { if (typingTimeoutRef.current) clearTimeout(typingTimeoutRef.current); }, []);📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| return ( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <form className="form" onSubmit={sendMessage}> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <input | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| className="input" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| type="text" | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| placeholder="Type a message..." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value={message} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onChange={handleChange} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| onKeyPress={(e) => (e.key === "Enter" ? sendMessage(e) : null)} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| /> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+24
to
+32
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Fix double-submit: Enter triggers both onKeyPress and form onSubmit. As written, pressing Enter will call sendMessage twice (once from onKeyPress and once from form submit), which can duplicate messages. Drop onKeyPress and rely on the form’s onSubmit. <input
className="input"
type="text"
placeholder="Type a message..."
value={message}
onChange={handleChange}
- onKeyPress={(e) => (e.key === "Enter" ? sendMessage(e) : null)}
/>📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| <button className="sendButton" type="submit"> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Send | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </button> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| </form> | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| }; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| export default Input; | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -4,48 +4,89 @@ const socketio = require('socket.io'); | |||||||||||||||||||||||||||||||||||||||||||||||||
| const cors = require('cors'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const { addUser, removeUser, getUser, getUsersInRoom } = require('./users'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const router = require('./router'); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| const app = express(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const server = http.createServer(app); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const io = socketio(server); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const path = require("path"); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(cors()); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| app.use(router); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| io.on('connect', (socket) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // User joins a room | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on('join', ({ name, room }, callback) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { error, user } = addUser({ id: socket.id, name, room }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if(error) return callback(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (error) return callback(error); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.join(user.room); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit('message', { user: 'admin', text: `${user.name}, welcome to room ${user.room}.`}); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.broadcast.to(user.room).emit('message', { user: 'admin', text: `${user.name} has joined!` }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| // ✅ Save user info on socket for typing events | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.data = { name: user.name, room: user.room }; | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.emit('message', { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| user: 'admin', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: `${user.name}, welcome to room ${user.room}.` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.broadcast.to(user.room).emit('message', { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| user: 'admin', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: `${user.name} has joined!` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('roomData', { room: user.room, users: getUsersInRoom(user.room) }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('roomData', { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| room: user.room, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| users: getUsersInRoom(user.room) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| callback(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ✅ User typing | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on('typing', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { name, room } = socket.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.to(room).emit('showTyping', `${name} is typing...`); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // ✅ User stopped typing | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on('stopTyping', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const { room } = socket.data; | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.to(room).emit('showTyping', ''); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+49
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Harden typing/stopTyping against pre-join races. If a client emits typing before join finishes, socket.data may be undefined, causing a crash. Guard and no-op when room isn’t available. socket.on('typing', () => {
- const { name, room } = socket.data;
- socket.to(room).emit('showTyping', `${name} is typing...`);
+ const data = socket.data || {};
+ if (!data.room) return;
+ socket.to(data.room).emit('showTyping', `${data.name} is typing...`);
});
socket.on('stopTyping', () => {
- const { room } = socket.data;
- socket.to(room).emit('showTyping', '');
+ const data = socket.data || {};
+ if (!data.room) return;
+ socket.to(data.room).emit('showTyping', '');
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // User sends message | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on('sendMessage', (message, callback) => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = getUser(socket.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('message', { user: user.name, text: message }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('message', { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| user: user.name, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: message | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| callback(); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+61
to
71
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🛠️ Refactor suggestion Guard sendMessage against missing user; ack errors explicitly. If getUser(socket.id) returns undefined, io.to(user.room) will throw. Return an error via callback instead. socket.on('sendMessage', (message, callback) => {
const user = getUser(socket.id);
- io.to(user.room).emit('message', {
- user: user.name,
- text: message
- });
+ if (!user) return callback && callback('User not found');
+ io.to(user.room).emit('message', { user: user.name, text: message });
callback();
});📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| // User disconnects | ||||||||||||||||||||||||||||||||||||||||||||||||||
| socket.on('disconnect', () => { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| const user = removeUser(socket.id); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| if(user) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('message', { user: 'Admin', text: `${user.name} has left.` }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('roomData', { room: user.room, users: getUsersInRoom(user.room)}); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| if (user) { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('message', { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| user: 'Admin', | ||||||||||||||||||||||||||||||||||||||||||||||||||
| text: `${user.name} has left.` | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| io.to(user.room).emit('roomData', { | ||||||||||||||||||||||||||||||||||||||||||||||||||
| room: user.room, | ||||||||||||||||||||||||||||||||||||||||||||||||||
| users: getUsersInRoom(user.room) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| }); | ||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||
| server.listen(process.env.PORT || 5000, () => console.log(`Server has started.`)); | ||||||||||||||||||||||||||||||||||||||||||||||||||
| server.listen(process.env.PORT || 5000, () => | ||||||||||||||||||||||||||||||||||||||||||||||||||
| console.log(`Server has started.`) | ||||||||||||||||||||||||||||||||||||||||||||||||||
| ); | ||||||||||||||||||||||||||||||||||||||||||||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
Avoid hardcoding the API URL in code; rely on REACT_APP_API_URL everywhere.
Since this file now defines REACT_APP_API_URL, ensure the client uses process.env.REACT_APP_API_URL instead of hardcoded "http://localhost:5000" (the AI summary notes an ENDPOINT constant in App.js). This keeps environments decoupled and avoids drift. Also consider adding .env.development and .env.production variants for clean overrides.
Reference: CRA env var conventions and caveats. (create-react-app.dev)
Run to find hardcoded endpoints:
🏁 Script executed:
Length of output: 148
🏁 Script executed:
Length of output: 716
Remove hardcoded API endpoints and switch to environment variables
The client currently hardcodes the API URL in two places:
Replace
with
(you may optionally include a fallback, e.g.
)
Additionally, create environment-specific files—.env.development and .env.production—to define
REACT_APP_API_URLper environment, following the Create React App conventions: https://create-react-app.dev/docs/adding-custom-environment-variables. This ensures each deployment uses the correct API endpoint and prevents configuration drift.🤖 Prompt for AI Agents