+53
README.md
+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
+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
+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
+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
+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
+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
+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
+9
client/src/components/Chat/NoMatchState.jsx
+9
client/src/components/Chat/SearchingState.jsx
+9
client/src/components/Chat/SearchingState.jsx
+44
client/src/components/UI/CustomAlert.jsx
+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
+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
+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
+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
+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
+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
+6
package-lock.json
+1
package.json
+1
package.json
···
1
+
{}
+1
-115
server/package-lock.json
+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
+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
+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
+1
-1
server/src/controllers/authController.js
server/src/controllers/messageController.js
server/src/controllers/messageController.js
This is a binary file and will not be displayed.
+16
-3
server/src/db/prisma.js
+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
+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
+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
+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
+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
+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
+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
-4
server/src/routes/messageRoutes.js
-6
server/src/routes/userRoutes.js
-6
server/src/routes/userRoutes.js
-31
server/src/test.http
-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
+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
-