A Place Where Random People Meet And Chat.

Compare changes

Choose any two refs to compare.

+53
README.md
··· 1 + # Not A Friend 2 + 3 + A application which provides a platform for random strangers to meet and chat. 4 + 5 + --- 6 + ## What This Project Demonstrates 7 + 8 + - Real-time communication using WebSockets 9 + - User session and state management 10 + - Friend request and relationship handling 11 + - Client-side data persistence with IndexedDB 12 + - Managing live connections and disconnections 13 + 14 + --- 15 + 16 + ## Tech Stack 17 + 18 + - **Frontend:** React 19 + - **Backend:** Node.js, Express 20 + - **Database:** PostgreSQL, Redis 21 + - **Client Side Storage**: IndexedDB 22 + - **Other Tools:** WebSockets, Prisma 23 + 24 + --- 25 + 26 + ## Getting Started 27 + 28 + Steps to run the project locally: 29 + 30 + 1. Installing Requirements 31 + 1. Node 32 + 2. PostgreSQL 33 + 3. Redis 34 + 35 + 2. Running Client 36 + 37 + ```bash 38 + git clone https://tangled.org/jai44.tngl.sh/NotAFriend 39 + cd NotAFriend 40 + cd client 41 + npm install 42 + npm run dev 43 + ``` 44 + 45 + 3. Running Server 46 + 47 + ``` bash 48 + cd ../server 49 + npm install 50 + cd prisma 51 + npx prisma generate 52 + npm run dev 53 + ```
+40 -1
client/package-lock.json
··· 9 9 "version": "0.0.0", 10 10 "dependencies": { 11 11 "axios": "^1.12.2", 12 + "idb": "^8.0.3", 12 13 "react": "^19.1.1", 13 14 "react-dom": "^19.1.1", 14 - "react-router-dom": "^7.9.2" 15 + "react-router-dom": "^7.9.2", 16 + "s": "^1.0.0", 17 + "ws": "^8.18.3" 15 18 }, 16 19 "devDependencies": { 17 20 "@eslint/js": "^9.36.0", ··· 2073 2076 "node": ">= 0.4" 2074 2077 } 2075 2078 }, 2079 + "node_modules/idb": { 2080 + "version": "8.0.3", 2081 + "resolved": "https://registry.npmjs.org/idb/-/idb-8.0.3.tgz", 2082 + "integrity": "sha512-LtwtVyVYO5BqRvcsKuB2iUMnHwPVByPCXFXOpuU96IZPPoPN6xjOGxZQ74pgSVVLQWtUOYgyeL4GE98BY5D3wg==", 2083 + "license": "ISC" 2084 + }, 2076 2085 "node_modules/ignore": { 2077 2086 "version": "5.3.2", 2078 2087 "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", ··· 2566 2575 "fsevents": "~2.3.2" 2567 2576 } 2568 2577 }, 2578 + "node_modules/s": { 2579 + "version": "1.0.0", 2580 + "resolved": "https://registry.npmjs.org/s/-/s-1.0.0.tgz", 2581 + "integrity": "sha512-Tz63UXhdEBvvIV6Q0a+AV2Dx1TPq+vVWNYBxyCT9TG0uqn9kySwFTjfq3C1YuGBRwYtt9Tof11L6GCKi88foqw==", 2582 + "license": "Apache-2.0", 2583 + "engines": { 2584 + "node": ">=0.8" 2585 + } 2586 + }, 2569 2587 "node_modules/scheduler": { 2570 2588 "version": "0.26.0", 2571 2589 "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", ··· 2776 2794 "license": "MIT", 2777 2795 "engines": { 2778 2796 "node": ">=0.10.0" 2797 + } 2798 + }, 2799 + "node_modules/ws": { 2800 + "version": "8.18.3", 2801 + "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", 2802 + "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", 2803 + "license": "MIT", 2804 + "engines": { 2805 + "node": ">=10.0.0" 2806 + }, 2807 + "peerDependencies": { 2808 + "bufferutil": "^4.0.1", 2809 + "utf-8-validate": ">=5.0.2" 2810 + }, 2811 + "peerDependenciesMeta": { 2812 + "bufferutil": { 2813 + "optional": true 2814 + }, 2815 + "utf-8-validate": { 2816 + "optional": true 2817 + } 2779 2818 } 2780 2819 }, 2781 2820 "node_modules/yocto-queue": {
+4 -1
client/package.json
··· 11 11 }, 12 12 "dependencies": { 13 13 "axios": "^1.12.2", 14 + "idb": "^8.0.3", 14 15 "react": "^19.1.1", 15 16 "react-dom": "^19.1.1", 16 - "react-router-dom": "^7.9.2" 17 + "react-router-dom": "^7.9.2", 18 + "s": "^1.0.0", 19 + "ws": "^8.18.3" 17 20 }, 18 21 "devDependencies": { 19 22 "@eslint/js": "^9.36.0",
+2 -1
client/src/App.jsx
··· 5 5 import Login from "./pages/Login.jsx"; 6 6 import Chat from "./pages/Chat.jsx"; 7 7 import Signup from "./pages/Signup.jsx"; 8 - 8 + import Friends from "./pages/Friends.jsx" 9 9 function App() { 10 10 return ( 11 11 <> ··· 15 15 <Route path="/login" element={<Login />} /> 16 16 <Route path="/signup" element={<Signup />} /> 17 17 <Route path="/chat" element={<Chat />} /> 18 + <Route path="/friends" element={<Friends />} /> 18 19 </Routes> 19 20 </> 20 21 );
+201
client/src/Hooks/useWebSocket.jsx
··· 1 + 2 + import { useCallback, useState, useEffect, useRef } from "react"; 3 + import { createFriend } from "../features/indexedDB/indexedDb"; 4 + import { useWebSocketContext } from "../helper/webSocketContext"; 5 + 6 + export function useWebSocket(userId) { 7 + 8 + // Storing Messages 9 + const [sentMessages, setSentMessages] = useState([]); 10 + const [receiveMessages, setReceiveMessages] = useState([]); 11 + 12 + // Connections 13 + const [isConnected, setIsConnected] = useState(false); 14 + const [isSearching, setIsSearching] = useState(false); 15 + const [matched, setMatched] = useState(false); 16 + const [terminated, setTerminated] = useState(false); 17 + const [isAccepted, setIsAccepted] = useState(null); 18 + const { isFriendShipExists, setIsFriendShipExists } = useWebSocketContext(); 19 + 20 + // receiver 21 + const [receiver, setReceiver] = useState( 22 + localStorage.getItem("receiver") 23 + ); 24 + 25 + //Friend Request 26 + const [isReceived, setIsReceived] = useState(false); 27 + const [showFriendRequest, setShowFriendRequest] = useState(false); 28 + 29 + //Socket 30 + const socketRef = useRef(null); 31 + 32 + const connect = useCallback(() => { 33 + //Prevent duplicate connections 34 + if (isConnected || !userId || socketRef.current) return; 35 + 36 + setIsSearching(true); 37 + console.log(`Attempting to connect to WebSocket with userId: ${userId}`); 38 + socketRef.current = new WebSocket(`ws://localhost:3000/?userId=${userId}`); 39 + 40 + socketRef.current.onopen = () => { 41 + console.log("Web Socket Is Connected"); 42 + setIsConnected(true); 43 + }; 44 + 45 + socketRef.current.onmessage = (payload) => { 46 + try { 47 + const data = JSON.parse(payload.data); 48 + 49 + switch (data.type) { 50 + case "matched": 51 + setIsSearching(false); 52 + setMatched(true); 53 + setReceiver(data.with); 54 + localStorage.setItem("receiver", data.with); 55 + break; 56 + 57 + case "disconnected": 58 + setIsSearching(true); 59 + setMatched(false); 60 + setReceiver(null); 61 + localStorage.removeItem("receiver"); 62 + break; 63 + 64 + case "no_match": 65 + setIsSearching(false); 66 + setMatched(false); 67 + setReceiver(null); 68 + localStorage.removeItem("receiver"); 69 + break; 70 + 71 + case "friendship_created": 72 + createFriend(data.id, data.messages.sentMessages, data.messages.receivedMessages) 73 + setIsAccepted(true) 74 + break; 75 + 76 + case "already friends": 77 + setIsFriendShipExists(true) 78 + break; 79 + 80 + default: 81 + if (data.action === 'message') { 82 + setReceiveMessages(prev => [...prev, data]); 83 + } else if (data.type === 'request') { 84 + setShowFriendRequest(true); 85 + setIsReceived(true); 86 + } break; 87 + } 88 + } catch (err) { 89 + console.log("Error Parsing Message:", err); 90 + } 91 + }; 92 + 93 + // Closing Connection 94 + socketRef.current.onclose = () => { 95 + setTerminated(true); 96 + setIsConnected(false); 97 + setIsSearching(false); 98 + }; 99 + 100 + socketRef.current.onerror = (err) => { 101 + console.error("WebSocket Error:", err); 102 + setIsSearching(false); 103 + }; 104 + 105 + }, [userId, isConnected]); 106 + 107 + // Disconnect 108 + const disconnect = useCallback(() => { 109 + if (socketRef.current) { 110 + socketRef.current.close(); 111 + socketRef.current = null; // Clear ref 112 + } 113 + setIsConnected(false); 114 + setIsSearching(false); 115 + }, []); 116 + 117 + // Reset 118 + const reset = useCallback(() => { 119 + if (socketRef.current) { 120 + socketRef.current.close(); 121 + socketRef.current = null; // Clear ref 122 + } 123 + setIsConnected(false); 124 + setIsSearching(false); 125 + setMatched(false); 126 + setReceiver(null); 127 + localStorage.removeItem("receiver"); 128 + setIsAccepted(null); 129 + }, []); 130 + 131 + //Sending Messages 132 + const sendMessage = useCallback((message) => { 133 + if (!socketRef.current || !matched) return; 134 + 135 + const payload = { 136 + id: userId, 137 + action: "message", 138 + type: "sent", 139 + message, 140 + time: new Date().toISOString(), 141 + }; 142 + setSentMessages(prev => [...prev, payload]); 143 + socketRef.current.send(JSON.stringify(payload)); 144 + }, [userId, matched]); 145 + 146 + // Sending Friend Requets 147 + const sendFriendRequest = useCallback(() => { 148 + if (!socketRef.current || !matched) return; 149 + 150 + const payload = { 151 + id: userId, 152 + type: "request", 153 + }; 154 + socketRef.current.send(JSON.stringify(payload)); 155 + }, [userId, matched]); 156 + 157 + // Responding Friend Requests 158 + const respondToRequest = useCallback((accepted) => { 159 + if (!socketRef.current) return; 160 + 161 + const payload = { 162 + id: userId, 163 + type: "response", 164 + action: accepted ? "accept" : "reject", 165 + }; 166 + 167 + socketRef.current.send(JSON.stringify(payload)); 168 + setShowFriendRequest(false); 169 + }, [userId]); 170 + 171 + // Closing Connection on unMount 172 + useEffect(() => { 173 + return () => { 174 + if (socketRef.current) { 175 + socketRef.current.close(); 176 + socketRef.current = null; // Clear ref to prevent memory leaks 177 + } 178 + }; 179 + }, []); 180 + 181 + return { 182 + sentMessages, 183 + receiveMessages, 184 + isConnected, 185 + isSearching, 186 + matched, 187 + receiver, 188 + terminated, 189 + showFriendRequest, 190 + connect, 191 + disconnect, 192 + reset, 193 + sendMessage, 194 + sendFriendRequest, 195 + isAccepted, 196 + isReceived, 197 + respondToRequest, 198 + isFriendShipExists, 199 + }; 200 + } 201 +
+95
client/src/components/Chat/ChatView.jsx
··· 1 + 2 + import { useState, useMemo } from "react"; 3 + import { useWebSocket } from "../../Hooks/useWebSocket"; 4 + import { useWebSocketContext } from "../../helper/webSocketContext"; 5 + 6 + export default function ChatView({ 7 + receiver, 8 + sentMessages, 9 + receiveMessages, 10 + onSendMessage, 11 + onSendRequest, 12 + isReceived, 13 + isAccepted, 14 + }) { 15 + const [currMessage, setCurrMessage] = useState(""); 16 + const [isSent, setIsSent] = useState(false); 17 + const userId = localStorage.getItem('userId') 18 + 19 + const { isFriendShipExists } = useWebSocketContext(); 20 + 21 + const sortedSentMessages = useMemo( 22 + () => [...sentMessages].sort((a, b) => new Date(a.time) - new Date(b.time)), 23 + [sentMessages] 24 + ); 25 + 26 + const sortedReceiveMessages = useMemo( 27 + () => [...receiveMessages].sort((a, b) => new Date(a.time) - new Date(b.time)), 28 + [receiveMessages] 29 + ); 30 + 31 + const mergeSorted = (a, b) => { 32 + const res = []; 33 + let i = 0, j = 0; 34 + 35 + while (i < a.length || j < b.length) { 36 + if (i === a.length) res.push(b[j++]); 37 + else if (j === b.length) res.push(a[i++]); 38 + else res.push( 39 + new Date(a[i].time) < new Date(b[j].time) ? a[i++] : b[j++] 40 + ); 41 + } 42 + return res; 43 + }; 44 + 45 + const sortedMessages = mergeSorted(sortedSentMessages, sortedReceiveMessages); 46 + 47 + const handleKeyPress = (e) => { 48 + if (e.key === "Enter" && currMessage.trim() !== "") { 49 + onSendMessage(currMessage); 50 + setCurrMessage(""); 51 + } 52 + }; 53 + 54 + const handleClick = () => { 55 + setIsSent(true); 56 + onSendRequest(); 57 + }; 58 + 59 + return ( 60 + <div> 61 + <h2>Chat with {receiver}</h2> 62 + 63 + <input 64 + type="text" 65 + value={currMessage} 66 + onChange={(e) => setCurrMessage(e.target.value)} 67 + onKeyDown={handleKeyPress} 68 + placeholder="Type a message..." 69 + /> 70 + 71 + <ul> 72 + {sortedMessages.map((msg, i) => ( 73 + <li key={i}> 74 + {msg.id}: {msg.message} 75 + </li> 76 + ))} 77 + </ul> 78 + 79 + {!isSent && !isReceived && !isFriendShipExists && ( 80 + <button onClick={handleClick}>Send Friend Request</button> 81 + )} 82 + 83 + {isSent && ( 84 + isAccepted == null ? ( 85 + <p>Request Pending</p> 86 + ) : isAccepted ? ( 87 + <p>Request Accepted</p> 88 + ) : ( 89 + <p>Request Rejected</p> 90 + ) 91 + )} 92 + </div> 93 + ); 94 + } 95 +
+13
client/src/components/Chat/InitialState.jsx
··· 1 + 2 + export default function InitialState({ onConnect}) { 3 + const handleClick = () => { 4 + const userId = localStorage.getItem("userId"); 5 + if (!userId) { 6 + window.alert("Please login first"); 7 + return; 8 + } 9 + onConnect(); 10 + }; 11 + 12 + return <button onClick={handleClick}>I want a friend</button>; 13 + }
+9
client/src/components/Chat/NoMatchState.jsx
··· 1 + 2 + export default function NoMatchState({ onTryAgain }) { 3 + return ( 4 + <div> 5 + <p>No match found. Try again later.</p> 6 + <button onClick={onTryAgain}>Try Again</button> 7 + </div> 8 + ); 9 + }
+9
client/src/components/Chat/SearchingState.jsx
··· 1 + 2 + export default function SearchingState({ onCancel }) { 3 + return ( 4 + <div> 5 + <p>Searching for a friend...</p> 6 + <button onClick={onCancel}>Cancel</button> 7 + </div> 8 + ); 9 + }
+2 -1
client/src/components/Navbar.jsx
··· 7 7 <Link to="/">Home</Link> | 8 8 <Link to="/login">Login</Link> | 9 9 <Link to="/signup">Signup</Link> | 10 - <Link to="/chat">Chat</Link> 10 + <Link to="/chat">Chat</Link> | 11 + <Link to="/friends">Friends</Link> 11 12 </nav> 12 13 ) 13 14 }
+44
client/src/components/UI/CustomAlert.jsx
··· 1 + 2 + export default function CustomAlert({ message, onAccept, onReject }) { 3 + const styles = { 4 + overlay: { 5 + position: "fixed", 6 + top: 0, 7 + left: 0, 8 + width: "100vw", 9 + height: "100vh", 10 + backgroundColor: "rgba(0, 0, 0, 0.5)", 11 + display: "flex", 12 + alignItems: "center", 13 + justifyContent: "center", 14 + zIndex: 9999, 15 + }, 16 + alertBox: { 17 + backgroundColor: "white", 18 + padding: "20px", 19 + borderRadius: "8px", 20 + boxShadow: "0 2px 10px rgba(0, 0, 0, 0.1)", 21 + textAlign: "center", 22 + }, 23 + button: { 24 + margin: "0 5px", 25 + padding: "8px 16px", 26 + cursor: "pointer", 27 + }, 28 + }; 29 + 30 + return ( 31 + <div style={styles.overlay}> 32 + <div style={styles.alertBox}> 33 + <p>{message}</p> 34 + <button style={styles.button} onClick={onAccept}> 35 + Accept 36 + </button> 37 + <button style={styles.button} onClick={onReject}> 38 + Reject 39 + </button> 40 + </div> 41 + </div> 42 + ); 43 + } 44 +
+71
client/src/features/indexedDB/indexedDb.jsx
··· 1 + import { openDB } from 'idb'; 2 + 3 + const user = localStorage.getItem("user"); 4 + const dbName = String(user); 5 + 6 + // Open or create a database 7 + export const db = await openDB(dbName, 1, { 8 + upgrade(db) { 9 + if (!db.objectStoreNames.contains("Friends")) { 10 + const store = db.createObjectStore("Friends", { keyPath: "id" }); 11 + console.log("Object store 'Friends' created."); 12 + } 13 + } 14 + }); 15 + 16 + // Store Messages Function 17 + export const storeMessages = async (data, friendId, type) => { 18 + const tx = db.transaction("Friends", "readwrite"); 19 + const store = tx.store; 20 + 21 + if (type === "receive") { 22 + friend.receivedMessages.push(data); 23 + } else if (type === "send") { 24 + friend.sentMessages.push(data); 25 + } else { 26 + console.warn("Invalid message type:", type); 27 + } 28 + 29 + await store.put(friend); 30 + await tx.done; 31 + 32 + console.log("Message stored successfully for friend:", friendId); 33 + }; 34 + 35 + // Create A Friend 36 + export const createFriend = async (friendId, receivedMessages = [], sentMessages = []) => { 37 + 38 + const tx = db.transaction("Friends", "readwrite"); 39 + const store = tx.store; 40 + 41 + // Create the friend object 42 + const friend = { 43 + id: friendId, 44 + receivedMessages, 45 + sentMessages, 46 + createdAt: new Date(), 47 + }; 48 + 49 + // Add or update the friend record 50 + await store.put(friend); 51 + 52 + await tx.done; 53 + 54 + console.log(`Friend ${friendId} saved locally withd sent and received messages.`); 55 + }; 56 + 57 + // Get Messages Function 58 + export const getMessages = async (friendId) => { 59 + const friend = await db.get("Friends", friendId); 60 + const sent = friend?.sentMessages || []; 61 + const received = friend?.receivedMessages || []; 62 + return [sent, received]; 63 + }; 64 + 65 + export const getFriends = async () => { 66 + 67 + if (!db) throw new Error("Database not initalized"); 68 + const friendIds = await db.getAllKeys("Friends"); 69 + 70 + return friendIds; 71 + }
+18
client/src/helper/webSocketContext.jsx
··· 1 + 2 + import { createContext, useContext, useState } from "react"; 3 + 4 + const WebSocketContext = createContext(); 5 + 6 + export function WebSocketProvider({ children }) { 7 + const [isFriendShipExists, setIsFriendShipExists] = useState(false); 8 + 9 + return ( 10 + <WebSocketContext.Provider value={{ isFriendShipExists, setIsFriendShipExists }}> 11 + {children} 12 + </WebSocketContext.Provider> 13 + ); 14 + } 15 + 16 + export function useWebSocketContext() { 17 + return useContext(WebSocketContext); 18 + }
+5 -2
client/src/main.jsx
··· 2 2 import ReactDOM from "react-dom/client" 3 3 import App from "./App.jsx" 4 4 import { BrowserRouter as Router } from "react-router-dom" 5 + import { WebSocketProvider } from "./helper/webSocketContext"; 6 + 5 7 6 8 ReactDOM.createRoot(document.getElementById("root")).render( 7 9 <Router> 8 - <App /> 10 + <WebSocketProvider> 11 + <App /> 12 + </WebSocketProvider> 9 13 </Router> 10 14 ) 11 -
+59 -126
client/src/pages/Chat.jsx
··· 1 - import { useEffect, useState, useRef } from "react"; 1 + 2 + import { useWebSocket } from "../Hooks/useWebSocket.jsx"; 3 + import CustomAlert from "../components/UI/CustomAlert.jsx"; 4 + import InitialState from "../components/Chat/InitialState.jsx"; 5 + import SearchingState from "../components/Chat/SearchingState.jsx"; 6 + import NoMatchState from "../components/Chat/NoMatchState.jsx"; 7 + import ChatView from "../components/Chat/ChatView.jsx"; 2 8 3 9 export default function Chat() { 10 + const userId = localStorage.getItem("userId"); 11 + const { 12 + sentMessages, 13 + receiveMessages, 14 + isConnected, 15 + isSearching, 16 + matched, 17 + isAccepted, 18 + isReceived, 19 + receiver, 20 + terminated, 21 + showFriendRequest, 22 + connect, 23 + disconnect, 24 + reset, 25 + sendMessage, 26 + sendFriendRequest, 27 + respondToRequest, 28 + } = useWebSocket(userId); 4 29 5 - const [messages, setMessages] = useState([]); 6 - const [terminated, setTerminated] = useState(false); 7 - const [currMessage, setCurrMessage] = useState(""); 8 - const [isConnected, setIsConnected] = useState(false); 9 - const [isSearching, setIsSearching] = useState(false); 10 - const [matched, setMatched] = useState(false); 11 - const [receiver, setReceiver] = useState(localStorage.getItem("receiver")); 12 - const socketRef = useRef(null); 30 + if (terminated) { 31 + return <h1>Chat Terminated</h1>; 32 + } 13 33 14 - const handleChange = (e) => setCurrMessage(e.target.value); 34 + const renderContent = () => { 35 + if (!isConnected) { 36 + return <InitialState onConnect={connect} />; 37 + } 15 38 16 - const handleKeyPress = (e) => { 17 - if (e.key === "Enter" && currMessage.trim() !== "" && matched) { 18 - const payload = { 19 - id: localStorage.getItem("userId"), 20 - message: currMessage 21 - }; 22 - socketRef.current.send(JSON.stringify(payload)); 23 - setCurrMessage(""); 39 + if (isSearching) { 40 + return <SearchingState onCancel={disconnect} />; 24 41 } 25 - }; 26 42 27 - const handleClick = () => { 28 - if (isConnected) return; 29 - 30 - const userId = localStorage.getItem("userId"); 31 - if (!userId) { 32 - alert("Please login first"); 33 - return; 43 + if (matched && receiver) { 44 + return ( 45 + <ChatView 46 + isReceived={isReceived} 47 + receiver={receiver} 48 + sentMessages={sentMessages} 49 + receiveMessages={receiveMessages} 50 + isAccepted={isAccepted} 51 + onSendMessage={sendMessage} 52 + onSendRequest={sendFriendRequest} 53 + /> 54 + ); 34 55 } 35 56 36 - setIsSearching(true); 37 - socketRef.current = new WebSocket(`ws://localhost:3000/?userId=${userId}`); 57 + if (isConnected && !isSearching) { 58 + return <NoMatchState onTryAgain={reset} />; 59 + } 38 60 39 - socketRef.current.onopen = () => { 40 - console.log("Connected to WebSocket"); 41 - setIsConnected(true); 42 - }; 43 - 44 - socketRef.current.onmessage = (e) => { 45 - try { 46 - const data = JSON.parse(e.data); 47 - 48 - if (data.type === "matched") { 49 - setIsSearching(false); 50 - setMatched(true); 51 - setReceiver(data.with); 52 - localStorage.setItem("receiver", data.with); 53 - console.log("Matched with:", data.with); 54 - } else if (data.type === "disconnected") { 55 - setMatched(false); 56 - setReceiver(null); 57 - localStorage.removeItem("receiver"); 58 - setIsSearching(true); 59 - console.log("Partner disconnected, searching for new match..."); 60 - } else if (data.type === "no_match") { 61 - setIsSearching(false); 62 - setMatched(false); 63 - setReceiver(null); 64 - localStorage.removeItem("receiver"); 65 - console.log("No match found"); 66 - } else { 67 - setMessages((prev) => [...prev, data]); 68 - } 69 - } catch (err) { 70 - console.error("Error parsing message:", err); 71 - setMessages((prev) => [...prev, e.data]); 72 - } 73 - }; 74 - 75 - socketRef.current.onclose = () => { 76 - setTerminated(true); 77 - setIsConnected(false); 78 - setIsSearching(false); 79 - }; 80 - 81 - socketRef.current.onerror = (err) => { 82 - console.error("WebSocket Error:", err); 83 - setIsSearching(false); 84 - }; 61 + return null; 85 62 }; 86 63 87 - useEffect(() => { 88 - return () => { 89 - if (socketRef.current) { 90 - socketRef.current.close(); 91 - } 92 - }; 93 - }, []); 94 - 95 - if (terminated) return <h1>Chat Terminated</h1>; 96 - 97 64 return ( 98 65 <div> 99 - {!isConnected ? ( 100 - <button onClick={handleClick}>I want a friend</button> 101 - ) : isSearching ? ( 102 - <div> 103 - <p>Searching for a friend...</p> 104 - <button onClick={() => { 105 - socketRef.current?.close(); 106 - setIsConnected(false); 107 - setIsSearching(false); 108 - }}>Cancel</button> 109 - </div> 110 - ) : matched && receiver ? ( 111 - <div> 112 - <h2>Chat with {receiver}</h2> 113 - <input 114 - type="text" 115 - value={currMessage} 116 - onChange={handleChange} 117 - onKeyDown={handleKeyPress} 118 - placeholder="Type a message..." 119 - /> 120 - <ul> 121 - {messages.map((msg, i) => ( 122 - <li key={i}> 123 - {typeof msg === 'string' ? msg : `${msg.id}: ${msg.message}`} 124 - </li> 125 - ))} 126 - </ul> 127 - </div> 128 - ) : isConnected && !isSearching ? ( 129 - <div> 130 - <p>No match found. Try again later.</p> 131 - <button onClick={() => { 132 - socketRef.current?.close(); 133 - setIsConnected(false); 134 - setIsSearching(false); 135 - setMatched(false); 136 - setReceiver(null); 137 - localStorage.removeItem("receiver"); 138 - }}>Try Again</button> 139 - </div> 140 - ) : null} 66 + {renderContent()} 67 + {showFriendRequest && ( 68 + <CustomAlert 69 + message={`Friend Request From ${receiver}`} 70 + onAccept={() => respondToRequest(true)} 71 + onReject={() => respondToRequest(false)} 72 + /> 73 + )} 141 74 </div> 142 75 ); 143 76 }
+44
client/src/pages/Friends.jsx
··· 1 + import { useEffect, useState } from "react"; 2 + import { useNavigate } from "react-router-dom"; 3 + import { getFriends } from "../features/indexedDB/indexedDb.jsx"; 4 + 5 + function Friends() { 6 + const [friends, setFriends] = useState([]); 7 + 8 + useEffect(() => { 9 + const loadFriends = async () => { 10 + try { 11 + const friendsList = await getFriends(); 12 + setFriends(friendsList); 13 + } catch (err) { 14 + console.error("Error loading friends:", err); 15 + } 16 + }; 17 + 18 + loadFriends(); 19 + }, []); 20 + 21 + const navigate = useNavigate(); 22 + 23 + const handleClick = (friend) => { 24 + navigate(`/chat?friendId=${friend.id}`); 25 + }; 26 + 27 + return ( 28 + <ul> 29 + {friends.length > 0 ? ( 30 + friends.map(friend => ( 31 + <li key={friend}> 32 + <a href="#" onClick={() => handleClick(friend)}> 33 + {friend} 34 + </a> 35 + </li> 36 + )) 37 + ) : ( 38 + <li>No friends found.</li> 39 + )} 40 + </ul> 41 + ); 42 + } 43 + 44 + export default Friends;
+6
package-lock.json
··· 1 + { 2 + "name": "Chat", 3 + "lockfileVersion": 3, 4 + "requires": true, 5 + "packages": {} 6 + }
+1
package.json
··· 1 + {}
+1 -115
server/package-lock.json
··· 13 13 "bcryptjs": "^3.0.2", 14 14 "cors": "^2.8.5", 15 15 "express": "^5.1.0", 16 - "http": "^0.0.1-security", 17 - "jsonwebtoken": "^9.0.2", 18 - "redis": "^5.8.3", 19 - "ws": "^8.18.3" 16 + "jsonwebtoken": "^9.0.2" 20 17 }, 21 18 "devDependencies": { 22 19 "prisma": "^6.16.2" ··· 107 104 "@prisma/debug": "6.16.2" 108 105 } 109 106 }, 110 - "node_modules/@redis/bloom": { 111 - "version": "5.8.3", 112 - "resolved": "https://registry.npmjs.org/@redis/bloom/-/bloom-5.8.3.tgz", 113 - "integrity": "sha512-1eldTzHvdW3Oi0TReb8m1yiFt8ZwyF6rv1NpZyG5R4TpCwuAdKQetBKoCw7D96tNFgsVVd6eL+NaGZZCqhRg4g==", 114 - "license": "MIT", 115 - "engines": { 116 - "node": ">= 18" 117 - }, 118 - "peerDependencies": { 119 - "@redis/client": "^5.8.3" 120 - } 121 - }, 122 - "node_modules/@redis/client": { 123 - "version": "5.8.3", 124 - "resolved": "https://registry.npmjs.org/@redis/client/-/client-5.8.3.tgz", 125 - "integrity": "sha512-MZVUE+l7LmMIYlIjubPosruJ9ltSLGFmJqsXApTqPLyHLjsJUSAbAJb/A3N34fEqean4ddiDkdWzNu4ZKPvRUg==", 126 - "license": "MIT", 127 - "dependencies": { 128 - "cluster-key-slot": "1.1.2" 129 - }, 130 - "engines": { 131 - "node": ">= 18" 132 - } 133 - }, 134 - "node_modules/@redis/json": { 135 - "version": "5.8.3", 136 - "resolved": "https://registry.npmjs.org/@redis/json/-/json-5.8.3.tgz", 137 - "integrity": "sha512-DRR09fy/u8gynHGJ4gzXYeM7D8nlS6EMv5o+h20ndTJiAc7RGR01fdk2FNjnn1Nz5PjgGGownF+s72bYG4nZKQ==", 138 - "license": "MIT", 139 - "engines": { 140 - "node": ">= 18" 141 - }, 142 - "peerDependencies": { 143 - "@redis/client": "^5.8.3" 144 - } 145 - }, 146 - "node_modules/@redis/search": { 147 - "version": "5.8.3", 148 - "resolved": "https://registry.npmjs.org/@redis/search/-/search-5.8.3.tgz", 149 - "integrity": "sha512-EMIvEeGRR2I0BJEz4PV88DyCuPmMT1rDtznlsHY3cKSDcc9vj0Q411jUnX0iU2vVowUgWn/cpySKjpXdZ8m+5g==", 150 - "license": "MIT", 151 - "engines": { 152 - "node": ">= 18" 153 - }, 154 - "peerDependencies": { 155 - "@redis/client": "^5.8.3" 156 - } 157 - }, 158 - "node_modules/@redis/time-series": { 159 - "version": "5.8.3", 160 - "resolved": "https://registry.npmjs.org/@redis/time-series/-/time-series-5.8.3.tgz", 161 - "integrity": "sha512-5Jwy3ilsUYQjzpE7WZ1lEeG1RkqQ5kHtwV1p8yxXHSEmyUbC/T/AVgyjMcm52Olj/Ov/mhDKjx6ndYUi14bXsw==", 162 - "license": "MIT", 163 - "engines": { 164 - "node": ">= 18" 165 - }, 166 - "peerDependencies": { 167 - "@redis/client": "^5.8.3" 168 - } 169 - }, 170 107 "node_modules/@standard-schema/spec": { 171 108 "version": "1.0.0", 172 109 "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.0.0.tgz", ··· 315 252 "consola": "^3.2.3" 316 253 } 317 254 }, 318 - "node_modules/cluster-key-slot": { 319 - "version": "1.1.2", 320 - "resolved": "https://registry.npmjs.org/cluster-key-slot/-/cluster-key-slot-1.1.2.tgz", 321 - "integrity": "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==", 322 - "license": "Apache-2.0", 323 - "engines": { 324 - "node": ">=0.10.0" 325 - } 326 - }, 327 255 "node_modules/confbox": { 328 256 "version": "0.2.2", 329 257 "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.2.tgz", ··· 766 694 "engines": { 767 695 "node": ">= 0.4" 768 696 } 769 - }, 770 - "node_modules/http": { 771 - "version": "0.0.1-security", 772 - "resolved": "https://registry.npmjs.org/http/-/http-0.0.1-security.tgz", 773 - "integrity": "sha512-RnDvP10Ty9FxqOtPZuxtebw1j4L/WiqNMDtuc1YMH1XQm5TgDRaR1G9u8upL6KD1bXHSp9eSXo/ED+8Q7FAr+g==" 774 697 }, 775 698 "node_modules/http-errors": { 776 699 "version": "2.0.0", ··· 1248 1171 "url": "https://paulmillr.com/funding/" 1249 1172 } 1250 1173 }, 1251 - "node_modules/redis": { 1252 - "version": "5.8.3", 1253 - "resolved": "https://registry.npmjs.org/redis/-/redis-5.8.3.tgz", 1254 - "integrity": "sha512-MfSrfV6+tEfTw8c4W0yFp6XWX8Il4laGU7Bx4kvW4uiYM1AuZ3KGqEGt1LdQHeD1nEyLpIWetZ/SpY3kkbgrYw==", 1255 - "license": "MIT", 1256 - "dependencies": { 1257 - "@redis/bloom": "5.8.3", 1258 - "@redis/client": "5.8.3", 1259 - "@redis/json": "5.8.3", 1260 - "@redis/search": "5.8.3", 1261 - "@redis/time-series": "5.8.3" 1262 - }, 1263 - "engines": { 1264 - "node": ">= 18" 1265 - } 1266 - }, 1267 1174 "node_modules/router": { 1268 1175 "version": "2.2.0", 1269 1176 "resolved": "https://registry.npmjs.org/router/-/router-2.2.0.tgz", ··· 1495 1402 "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1496 1403 "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", 1497 1404 "license": "ISC" 1498 - }, 1499 - "node_modules/ws": { 1500 - "version": "8.18.3", 1501 - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", 1502 - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", 1503 - "license": "MIT", 1504 - "engines": { 1505 - "node": ">=10.0.0" 1506 - }, 1507 - "peerDependencies": { 1508 - "bufferutil": "^4.0.1", 1509 - "utf-8-validate": ">=5.0.2" 1510 - }, 1511 - "peerDependenciesMeta": { 1512 - "bufferutil": { 1513 - "optional": true 1514 - }, 1515 - "utf-8-validate": { 1516 - "optional": true 1517 - } 1518 - } 1519 1405 } 1520 1406 } 1521 1407 }
+2 -5
server/package.json
··· 5 5 "main": "index.js", 6 6 "type": "module", 7 7 "scripts": { 8 - "dev": "node src/server.js", 8 + "dev" : "node src/server.js", 9 9 "test": "echo \"Error: no test specified\" && exit 1" 10 10 }, 11 11 "keywords": [], ··· 16 16 "bcryptjs": "^3.0.2", 17 17 "cors": "^2.8.5", 18 18 "express": "^5.1.0", 19 - "http": "^0.0.1-security", 20 - "jsonwebtoken": "^9.0.2", 21 - "redis": "^5.8.3", 22 - "ws": "^8.18.3" 19 + "jsonwebtoken": "^9.0.2" 23 20 }, 24 21 "devDependencies": { 25 22 "prisma": "^6.16.2"
+1 -1
server/src/app.js
··· 8 8 9 9 const app = express() 10 10 11 - 12 11 //CORS For Getting Requests From Frontend 13 12 app.use(cors()) 14 13 ··· 21 20 // Connect user routes โ†’ any request starting with /api/users 22 21 //app.use("/api/users", userRoutes) 23 22 app.use("/api/auth", authRoutes) 23 + //app.use("api/messages", chatRoutes) 24 24 25 25 26 26 // Fallback route
+1 -1
server/src/controllers/authController.js
··· 60 60 } 61 61 62 62 // Verifying Password 63 - const ok = await bcrypt.compare(password, user.password); 63 + const ok = await bcrypt.compare(password, user.password); 64 64 if (!ok) { 65 65 return res.status(401).json({ success: false, message: "invalid credentials" }); 66 66 }
server/src/controllers/messageController.js

This is a binary file and will not be displayed.

-3
server/src/controllers/userController.js
··· 1 - const findUser = () => { 2 - 3 - }
+16 -3
server/src/db/prisma.js
··· 1 1 2 - import { PrismaClient } from "./../generated/prisma/client.js" 3 - const prisma = new PrismaClient() 2 + import { PrismaClient } from "./../generated/prisma/client.js"; 3 + const prisma = new PrismaClient(); 4 + 5 + export const existingFriendship = async (userId, friendId) => { 6 + console.log("Exists") 7 + return await prisma.friendship.findFirst({ 8 + where: { 9 + OR: [ 10 + { userId: Number(userId), friendId: Number(friendId) }, 11 + { userId: Number(friendId), friendId: Number(userId) }, 12 + ], 13 + }, 14 + }); 15 + }; 16 + 17 + export default prisma; 4 18 5 - export default prisma
+29
server/src/helper/createFriendship.js
··· 1 + import { 2 + getMessages 3 + } from "./../redisClient.js"; 4 + import prisma from "../db/prisma.js" 5 + 6 + 7 + async function createFriendship(userId, friendId) { 8 + await prisma.$transaction(async (tx) => { 9 + await tx.friendship.createMany({ 10 + data: [ 11 + { userId, friendId, createdAt: new Date() }, 12 + { userId: friendId, friendId: userId, createdAt: new Date() }, 13 + ], 14 + }); 15 + 16 + console.log(`Friendship created between ${userId} and ${friendId}`); 17 + }); 18 + 19 + // Fetch stored messages in both directions 20 + const [sentMessages, receivedMessages] = await Promise.all([ 21 + getMessages(userId, friendId, "sent"), 22 + getMessages(userId, friendId, "received"), 23 + ]); 24 + 25 + return { sentMessages, receivedMessages }; 26 + } 27 + 28 + 29 + export default createFriendship
+27
server/src/helper/createFriendship.jsx
··· 1 + import { 2 + getMessages 3 + } from "./redisClient.js"; 4 + 5 + async function createFriendship(userId, friendId) { 6 + await prisma.$transaction(async (tx) => { 7 + await tx.friendship.createMany({ 8 + data: [ 9 + { userId, friendId, createdAt: new Date() }, 10 + { userId: friendId, friendId: userId, createdAt: new Date() }, 11 + ], 12 + }); 13 + 14 + console.log(`Friendship created between ${userId} and ${friendId}`); 15 + }); 16 + 17 + // Fetch stored messages in both directions 18 + const [sentMessages, receivedMessages] = await Promise.all([ 19 + getMessages(userId, friendId, "sent"), 20 + getMessages(userId, friendId, "received"), 21 + ]); 22 + 23 + return { sentMessages, receivedMessages }; 24 + } 25 + 26 + 27 + export default createFriendship
+35
server/src/prisma/migrations/20251019100015_/migration.sql
··· 1 + -- CreateTable 2 + CREATE TABLE "public"."Messages" ( 3 + "id" SERIAL NOT NULL, 4 + "senderId" INTEGER NOT NULL, 5 + "receiverId" INTEGER NOT NULL, 6 + "message" TEXT NOT NULL, 7 + "time" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 8 + 9 + CONSTRAINT "Messages_pkey" PRIMARY KEY ("id") 10 + ); 11 + 12 + -- CreateTable 13 + CREATE TABLE "public"."Friendship" ( 14 + "id" SERIAL NOT NULL, 15 + "userId" INTEGER NOT NULL, 16 + "friendId" INTEGER NOT NULL, 17 + "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, 18 + 19 + CONSTRAINT "Friendship_pkey" PRIMARY KEY ("id") 20 + ); 21 + 22 + -- CreateIndex 23 + CREATE UNIQUE INDEX "Friendship_userId_friendId_key" ON "public"."Friendship"("userId", "friendId"); 24 + 25 + -- AddForeignKey 26 + ALTER TABLE "public"."Messages" ADD CONSTRAINT "Messages_senderId_fkey" FOREIGN KEY ("senderId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 27 + 28 + -- AddForeignKey 29 + ALTER TABLE "public"."Messages" ADD CONSTRAINT "Messages_receiverId_fkey" FOREIGN KEY ("receiverId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 30 + 31 + -- AddForeignKey 32 + ALTER TABLE "public"."Friendship" ADD CONSTRAINT "Friendship_userId_fkey" FOREIGN KEY ("userId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE; 33 + 34 + -- AddForeignKey 35 + ALTER TABLE "public"."Friendship" ADD CONSTRAINT "Friendship_friendId_fkey" FOREIGN KEY ("friendId") REFERENCES "public"."User"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
+14
server/src/prisma/migrations/20251020140702_init/migration.sql
··· 1 + /* 2 + Warnings: 3 + 4 + - You are about to drop the `Messages` table. If the table is not empty, all the data it contains will be lost. 5 + 6 + */ 7 + -- DropForeignKey 8 + ALTER TABLE "public"."Messages" DROP CONSTRAINT "Messages_receiverId_fkey"; 9 + 10 + -- DropForeignKey 11 + ALTER TABLE "public"."Messages" DROP CONSTRAINT "Messages_senderId_fkey"; 12 + 13 + -- DropTable 14 + DROP TABLE "public"."Messages";
+16
server/src/prisma/schema.prisma
··· 19 19 username String @unique 20 20 name String 21 21 password String 22 + 23 + friendships Friendship[] @relation("UserFriendships") // friendships this user initiated 24 + friends Friendship[] @relation("FriendUser") // friendships where this user is the friend 25 + } 26 + 27 + model Friendship { 28 + id Int @id @default(autoincrement()) 29 + userId Int 30 + friendId Int 31 + createdAt DateTime @default(now()) 32 + 33 + // Each friendship connects two users: 34 + user User @relation("UserFriendships", fields: [userId], references: [id]) 35 + friend User @relation("FriendUser", fields: [friendId], references: [id]) 36 + 37 + @@unique([userId, friendId]) 22 38 }
+7 -1
server/src/redisClient.js
··· 93 93 await redis.expire(field, 3600); // 1 hour expiry 94 94 }; 95 95 96 - export const getTime = () => new Date().toISOString(); 97 96 97 + export const getMessages = async (user, friend, type) => { 98 + const field = `${user}:${friend}:${type}`; 99 + const messages = await redis.lRange(field, 0, -1); // Get all messages 100 + return messages.map(msg => JSON.parse(msg)); // Return list of JSON objects 101 + }; 102 + 103 + export const getTime = () => new Date().toISOString(); 98 104
-4
server/src/routes/messageRoutes.js
··· 1 - 2 - export const sendMessage = async (id, message) => { 3 - 4 - }
-6
server/src/routes/userRoutes.js
··· 1 - import express from "express" 2 1 3 - const router = express.Router() 4 - 5 - router.post('/finduser', findUser) 6 - 7 - export default router
-31
server/src/test.http
··· 1 - ### Variables 2 - 3 - @base = http://localhost:3000 4 - @name = Test User 5 - @password = StrongP@ssw0rd! 6 - @username = user_{{timestamp}} 7 - 8 - 9 - ### Signup 10 - 11 - POST {{base}}/api/auth/signup HTTP/1.1 12 - Content-Type: application/json 13 - Accept: application/json 14 - 15 - { 16 - "name": "{{name}}", 17 - "password": "{{password}}", 18 - "username": "{{username}}" 19 - } 20 - 21 - 22 - ### Login 23 - 24 - POST {{base}}/api/auth/login HTTP/1.1 25 - Content-Type: application/json 26 - Accept: application/json 27 - 28 - { 29 - "password": "{{password}}", 30 - "username": "{{username}}" 31 - }
+97 -54
server/src/webSocket.js
··· 1 1 2 - import { WebSocketServer } from "ws"; 3 - import { redis, addUser, removeUser, getRandom, address, addMessage, getTime } from "./redisClient.js"; 2 + import WebSocket, { WebSocketServer } from "ws"; 3 + import { 4 + addUser, 5 + removeUser, 6 + getRandom, 7 + address, 8 + getTime, 9 + addMessage, 10 + } from "./redisClient.js"; 11 + import createFriendship from "./helper/createFriendship.js"; 12 + import { existingFriendship } from "./db/prisma.js"; 13 + 14 + const makeMessagePairs = (sent, received) => [ 15 + { sentMessages: sent, receivedMessages: received }, 16 + { sentMessages: received, receivedMessages: sent }, 17 + ]; 18 + 19 + const sendIfOpen = (socket, payload) => { 20 + if (socket && socket.readyState === WebSocket.OPEN) { 21 + socket.send(JSON.stringify(payload)); 22 + } 23 + }; 24 + 25 + const onAccept = async (userId, friendId) => { 26 + const { sentMessages, receivedMessages } = await createFriendship(userId, friendId); 27 + return makeMessagePairs(sentMessages, receivedMessages); 28 + }; 4 29 5 30 const webSocketServer = (server) => { 6 31 const ws = new WebSocketServer({ server }); 7 32 8 33 ws.on("connection", async (socket, request) => { 9 - try { 10 - const url = new URL(request.url, `http://${request.headers.host}`); 11 - const userId = url.searchParams.get("userId"); 34 + const params = new URLSearchParams(request.url.replace("/?", "")); 35 + const userId = params.get("userId"); 12 36 13 - if (!userId) { 14 - socket.close(); 15 - return; 16 - } 37 + if (!userId) { 38 + socket.close(); 39 + return; 40 + } 17 41 18 - console.log(`Socket connected: ${userId}`); 19 - await addUser(userId, socket); 42 + console.log(`Socket connected: ${userId}`); 43 + 44 + const userSocket = socket; 45 + await addUser(userId, userSocket); 46 + 47 + const friendId = await getRandom(userId); 48 + const receiverSocket = friendId ? await address.get(friendId) : null; 49 + 50 + const isFriends = await existingFriendship(userId, friendId) 20 51 21 - const friendId = await getRandom(userId); 22 - if (friendId) { 23 - const friendSocket = address.get(friendId); 24 - if (friendSocket) { 25 - console.log(`Matched ${userId} with ${friendId}`); 26 - socket.send(JSON.stringify({ type: "matched", with: friendId })); 27 - friendSocket.send(JSON.stringify({ type: "matched", with: userId })); 28 - } else { 29 - console.log(`Friend ${friendId} socket not found, removing pairing`); 30 - await redis.hDel("friends", userId); 31 - await redis.hDel("friends", friendId); 32 - await redis.sAdd("activeUsers", userId); 33 - await redis.sAdd("activeUsers", friendId); 34 - } 35 - } else { 36 - console.log(`No match found for ${userId} within timeout`); 37 - socket.send(JSON.stringify({ type: "no_match" })); 38 - } 52 + if (receiverSocket && isFriends) { 53 + console.log("Already Friends") 54 + userSocket.send(JSON.stringify({ type: "already friends" })); 55 + receiverSocket.send(JSON.stringify({ type: "already friends" })); 56 + } 57 + if (receiverSocket) { 58 + userSocket.send(JSON.stringify({ type: "matched", with: String(friendId) })); 59 + receiverSocket.send(JSON.stringify({ type: "matched", with: String(userId) })); 60 + 61 + console.log("Friendship Established") 62 + 63 + } else { 64 + userSocket.send(JSON.stringify({ type: "no_match" })); 65 + } 66 + 67 + socket.on("message", async (payload) => { 68 + const data = JSON.parse(payload.toString()); 69 + data.time = getTime(); 70 + 71 + if (data.type === "response" && !isFriends) { 39 72 40 - socket.on("message", async (message) => { 41 - let payload; 42 - try { 43 - payload = JSON.parse(message.toString()); 44 - } catch (err) { 45 - console.error("Invalid JSON:", err); 46 - return; 47 - } 73 + // Request Accepted 74 + if (data.action === 'accept') { 75 + const messages = await onAccept(Number(userId), Number(friendId)); 48 76 49 - payload.time = getTime(); 77 + sendIfOpen(userSocket, { 78 + id: friendId, 79 + type: "response", 80 + action: "friendship created", 81 + messages: messages[0], 82 + }); 50 83 51 - const friendId = await redis.hGet("friends", userId); 52 - const receiverSocket = address.get(friendId); 84 + sendIfOpen(receiverSocket, { 85 + id: userId, 86 + type: "response", 87 + action: "friendship created", 88 + messages: messages[1], 89 + }); 53 90 54 - if (receiverSocket) { 55 - receiverSocket.send(JSON.stringify(payload)); 91 + return 56 92 } 57 93 58 - await addMessage(payload, userId, "send"); 59 - await addMessage(payload, friendId, "receive"); 60 - }); 94 + //Request Declined 95 + sendIfOpen(userSocket, { 96 + id: friendId, 97 + type: "response", 98 + action: "friend request declined", 99 + }) 100 + } else { 101 + 102 + } 103 + 104 + await addMessage(data, userId, "sent"); 105 + await addMessage(data, friendId, "receive"); 61 106 62 - socket.on("close", async () => { 63 - console.log(`Connection closed: ${userId}`); 64 - await removeUser(userId); 65 - }); 107 + const receiverSocket = address.get(friendId); 108 + if (receiverSocket) receiverSocket.send(JSON.stringify(data)); 109 + }); 66 110 67 - } catch (err) { 68 - console.error("Connection error:", err); 69 - socket.close(); 70 - } 111 + socket.on("close", async () => { 112 + console.log(`Connection closed: ${userId}`); 113 + await removeUser(userId); 114 + }); 71 115 }); 72 116 }; 73 117 74 118 export default webSocketServer; 75 -