diff --git a/examples/react-private-messaging/.eslintrc.cjs b/examples/react-private-messaging/.eslintrc.cjs
new file mode 100644
index 0000000000..4dcb43901a
--- /dev/null
+++ b/examples/react-private-messaging/.eslintrc.cjs
@@ -0,0 +1,20 @@
+module.exports = {
+ root: true,
+ env: { browser: true, es2020: true },
+ extends: [
+ 'eslint:recommended',
+ 'plugin:react/recommended',
+ 'plugin:react/jsx-runtime',
+ 'plugin:react-hooks/recommended',
+ ],
+ ignorePatterns: ['dist', '.eslintrc.cjs'],
+ parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
+ settings: { react: { version: '18.2' } },
+ plugins: ['react-refresh'],
+ rules: {
+ 'react-refresh/only-export-components': [
+ 'warn',
+ { allowConstantExport: true },
+ ],
+ },
+}
diff --git a/examples/react-private-messaging/.gitignore b/examples/react-private-messaging/.gitignore
new file mode 100644
index 0000000000..a547bf36d8
--- /dev/null
+++ b/examples/react-private-messaging/.gitignore
@@ -0,0 +1,24 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+dist
+dist-ssr
+*.local
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+.DS_Store
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
diff --git a/examples/react-private-messaging/LICENSE b/examples/react-private-messaging/LICENSE
new file mode 100644
index 0000000000..190bfbe0ea
--- /dev/null
+++ b/examples/react-private-messaging/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2023 qzhang1
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/examples/react-private-messaging/README.md b/examples/react-private-messaging/README.md
new file mode 100644
index 0000000000..d8ac0bd0d6
--- /dev/null
+++ b/examples/react-private-messaging/README.md
@@ -0,0 +1,23 @@
+# Private Message in React + Vite
+
+This is an implementation of the Private messaging example in React following
+Socket.io recommendations https://socket.io/how-to/use-with-react
+
+## Running the frontend
+
+```
+npm install
+npm run dev
+```
+
+### Running the server
+
+```
+cd server
+npm install
+npm start
+```
+
+Here's what the interface looks like
+![Alt text](image.png)
+
diff --git a/examples/react-private-messaging/image.png b/examples/react-private-messaging/image.png
new file mode 100644
index 0000000000..124e43934e
Binary files /dev/null and b/examples/react-private-messaging/image.png differ
diff --git a/examples/react-private-messaging/index.html b/examples/react-private-messaging/index.html
new file mode 100644
index 0000000000..c49a808a7b
--- /dev/null
+++ b/examples/react-private-messaging/index.html
@@ -0,0 +1,12 @@
+
+
+
+
+
+ React Socket.io
+
+
+
+
+
+
diff --git a/examples/react-private-messaging/package.json b/examples/react-private-messaging/package.json
new file mode 100644
index 0000000000..58d66c08ab
--- /dev/null
+++ b/examples/react-private-messaging/package.json
@@ -0,0 +1,31 @@
+{
+ "name": "basic-crud-socket-app",
+ "private": true,
+ "version": "0.0.0",
+ "type": "module",
+ "scripts": {
+ "dev": "vite",
+ "build": "vite build",
+ "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
+ "preview": "vite preview"
+ },
+ "dependencies": {
+ "@emotion/react": "^11.11.1",
+ "@emotion/styled": "^11.11.0",
+ "@mui/icons-material": "^5.15.0",
+ "@mui/material": "^5.15.0",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "socket.io-client": "^4.7.2"
+ },
+ "devDependencies": {
+ "@types/react": "^18.2.43",
+ "@types/react-dom": "^18.2.17",
+ "@vitejs/plugin-react": "^4.2.1",
+ "eslint": "^8.55.0",
+ "eslint-plugin-react": "^7.33.2",
+ "eslint-plugin-react-hooks": "^4.6.0",
+ "eslint-plugin-react-refresh": "^0.4.5",
+ "vite": "^5.0.8"
+ }
+}
diff --git a/examples/react-private-messaging/server/cluster.js b/examples/react-private-messaging/server/cluster.js
new file mode 100644
index 0000000000..e3bad09f8c
--- /dev/null
+++ b/examples/react-private-messaging/server/cluster.js
@@ -0,0 +1,31 @@
+const cluster = require("cluster");
+const http = require("http");
+const { setupMaster } = require("@socket.io/sticky");
+
+const WORKERS_COUNT = 4;
+
+if (cluster.isMaster) {
+ console.log(`Master ${process.pid} is running`);
+
+ for (let i = 0; i < WORKERS_COUNT; i++) {
+ cluster.fork();
+ }
+
+ cluster.on("exit", (worker) => {
+ console.log(`Worker ${worker.process.pid} died`);
+ cluster.fork();
+ });
+
+ const httpServer = http.createServer();
+ setupMaster(httpServer, {
+ loadBalancingMethod: "least-connection", // either "random", "round-robin" or "least-connection"
+ });
+ const PORT = process.env.PORT || 3000;
+
+ httpServer.listen(PORT, () =>
+ console.log(`server listening at http://localhost:${PORT}`)
+ );
+} else {
+ console.log(`Worker ${process.pid} started`);
+ require("./index");
+}
diff --git a/examples/react-private-messaging/server/docker-compose.yml b/examples/react-private-messaging/server/docker-compose.yml
new file mode 100644
index 0000000000..4845950cf6
--- /dev/null
+++ b/examples/react-private-messaging/server/docker-compose.yml
@@ -0,0 +1,7 @@
+version: "3"
+
+services:
+ redis:
+ image: redis:5
+ ports:
+ - "6379:6379"
diff --git a/examples/react-private-messaging/server/index.js b/examples/react-private-messaging/server/index.js
new file mode 100644
index 0000000000..1ab99be5c4
--- /dev/null
+++ b/examples/react-private-messaging/server/index.js
@@ -0,0 +1,125 @@
+const httpServer = require("http").createServer();
+const Redis = require("ioredis");
+const redisClient = new Redis();
+const io = require("socket.io")(httpServer, {
+ cors: {
+ origin: "http://localhost:8080",
+ },
+ adapter: require("socket.io-redis")({
+ pubClient: redisClient,
+ subClient: redisClient.duplicate(),
+ }),
+});
+
+const { setupWorker } = require("@socket.io/sticky");
+const crypto = require("crypto");
+const randomId = () => crypto.randomBytes(8).toString("hex");
+
+const { RedisSessionStore } = require("./sessionStore");
+const sessionStore = new RedisSessionStore(redisClient);
+
+const { RedisMessageStore } = require("./messageStore");
+const messageStore = new RedisMessageStore(redisClient);
+
+io.use(async (socket, next) => {
+ const sessionID = socket.handshake.auth.sessionID;
+ if (sessionID) {
+ const session = await sessionStore.findSession(sessionID);
+ if (session) {
+ socket.sessionID = sessionID;
+ socket.userID = session.userID;
+ socket.username = session.username;
+ return next();
+ }
+ }
+ const username = socket.handshake.auth.username;
+ if (!username) {
+ return next(new Error("invalid username"));
+ }
+ socket.sessionID = randomId();
+ socket.userID = randomId();
+ socket.username = username;
+ next();
+});
+
+io.on("connection", async (socket) => {
+ // persist session
+ sessionStore.saveSession(socket.sessionID, {
+ userID: socket.userID,
+ username: socket.username,
+ connected: true,
+ });
+
+ // emit session details
+ socket.emit("session", {
+ sessionID: socket.sessionID,
+ userID: socket.userID,
+ });
+
+ // join the "userID" room
+ socket.join(socket.userID);
+
+ // fetch existing users
+ const users = [];
+ const [messages, sessions] = await Promise.all([
+ messageStore.findMessagesForUser(socket.userID),
+ sessionStore.findAllSessions(),
+ ]);
+ const messagesPerUser = new Map();
+ messages.forEach((message) => {
+ const { from, to } = message;
+ const otherUser = socket.userID === from ? to : from;
+ if (messagesPerUser.has(otherUser)) {
+ messagesPerUser.get(otherUser).push(message);
+ } else {
+ messagesPerUser.set(otherUser, [message]);
+ }
+ });
+
+ sessions.forEach((session) => {
+ users.push({
+ userID: session.userID,
+ username: session.username,
+ connected: session.connected,
+ messages: messagesPerUser.get(session.userID) || [],
+ });
+ });
+ socket.emit("users", users);
+
+ // notify existing users
+ socket.broadcast.emit("user connected", {
+ userID: socket.userID,
+ username: socket.username,
+ connected: true,
+ messages: [],
+ });
+
+ // forward the private message to the right recipient (and to other tabs of the sender)
+ socket.on("private message", ({ content, to }) => {
+ const message = {
+ content,
+ from: socket.userID,
+ to,
+ };
+ socket.to(to).to(socket.userID).emit("private message", message);
+ messageStore.saveMessage(message);
+ });
+
+ // notify users upon disconnection
+ socket.on("disconnect", async () => {
+ const matchingSockets = await io.in(socket.userID).allSockets();
+ const isDisconnected = matchingSockets.size === 0;
+ if (isDisconnected) {
+ // notify other users
+ socket.broadcast.emit("user disconnected", socket.userID);
+ // update the connection status of the session
+ sessionStore.saveSession(socket.sessionID, {
+ userID: socket.userID,
+ username: socket.username,
+ connected: false,
+ });
+ }
+ });
+});
+
+setupWorker(io);
diff --git a/examples/react-private-messaging/server/messageStore.js b/examples/react-private-messaging/server/messageStore.js
new file mode 100644
index 0000000000..60ab0f6f72
--- /dev/null
+++ b/examples/react-private-messaging/server/messageStore.js
@@ -0,0 +1,54 @@
+/* abstract */ class MessageStore {
+ saveMessage(message) {}
+ findMessagesForUser(userID) {}
+}
+
+class InMemoryMessageStore extends MessageStore {
+ constructor() {
+ super();
+ this.messages = [];
+ }
+
+ saveMessage(message) {
+ this.messages.push(message);
+ }
+
+ findMessagesForUser(userID) {
+ return this.messages.filter(
+ ({ from, to }) => from === userID || to === userID
+ );
+ }
+}
+
+const CONVERSATION_TTL = 24 * 60 * 60;
+
+class RedisMessageStore extends MessageStore {
+ constructor(redisClient) {
+ super();
+ this.redisClient = redisClient;
+ }
+
+ saveMessage(message) {
+ const value = JSON.stringify(message);
+ this.redisClient
+ .multi()
+ .rpush(`messages:${message.from}`, value)
+ .rpush(`messages:${message.to}`, value)
+ .expire(`messages:${message.from}`, CONVERSATION_TTL)
+ .expire(`messages:${message.to}`, CONVERSATION_TTL)
+ .exec();
+ }
+
+ findMessagesForUser(userID) {
+ return this.redisClient
+ .lrange(`messages:${userID}`, 0, -1)
+ .then((results) => {
+ return results.map((result) => JSON.parse(result));
+ });
+ }
+}
+
+module.exports = {
+ InMemoryMessageStore,
+ RedisMessageStore,
+};
diff --git a/examples/react-private-messaging/server/package.json b/examples/react-private-messaging/server/package.json
new file mode 100644
index 0000000000..6a0310bcd7
--- /dev/null
+++ b/examples/react-private-messaging/server/package.json
@@ -0,0 +1,17 @@
+{
+ "name": "server",
+ "version": "1.0.0",
+ "description": "",
+ "main": "index.js",
+ "scripts": {
+ "start": "node cluster.js"
+ },
+ "author": "Damien Arrachequesne ",
+ "license": "MIT",
+ "dependencies": {
+ "@socket.io/sticky": "^1.0.0",
+ "ioredis": "^4.22.0",
+ "socket.io": "^4.0.0",
+ "socket.io-redis": "^6.0.1"
+ }
+}
diff --git a/examples/react-private-messaging/server/sessionStore.js b/examples/react-private-messaging/server/sessionStore.js
new file mode 100644
index 0000000000..0ace3f4eb5
--- /dev/null
+++ b/examples/react-private-messaging/server/sessionStore.js
@@ -0,0 +1,89 @@
+/* abstract */ class SessionStore {
+ findSession(id) {}
+ saveSession(id, session) {}
+ findAllSessions() {}
+}
+
+class InMemorySessionStore extends SessionStore {
+ constructor() {
+ super();
+ this.sessions = new Map();
+ }
+
+ findSession(id) {
+ return this.sessions.get(id);
+ }
+
+ saveSession(id, session) {
+ this.sessions.set(id, session);
+ }
+
+ findAllSessions() {
+ return [...this.sessions.values()];
+ }
+}
+
+const SESSION_TTL = 24 * 60 * 60;
+const mapSession = ([userID, username, connected]) =>
+ userID ? { userID, username, connected: connected === "true" } : undefined;
+
+class RedisSessionStore extends SessionStore {
+ constructor(redisClient) {
+ super();
+ this.redisClient = redisClient;
+ }
+
+ findSession(id) {
+ return this.redisClient
+ .hmget(`session:${id}`, "userID", "username", "connected")
+ .then(mapSession);
+ }
+
+ saveSession(id, { userID, username, connected }) {
+ this.redisClient
+ .multi()
+ .hset(
+ `session:${id}`,
+ "userID",
+ userID,
+ "username",
+ username,
+ "connected",
+ connected
+ )
+ .expire(`session:${id}`, SESSION_TTL)
+ .exec();
+ }
+
+ async findAllSessions() {
+ const keys = new Set();
+ let nextIndex = 0;
+ do {
+ const [nextIndexAsStr, results] = await this.redisClient.scan(
+ nextIndex,
+ "MATCH",
+ "session:*",
+ "COUNT",
+ "100"
+ );
+ nextIndex = parseInt(nextIndexAsStr, 10);
+ results.forEach((s) => keys.add(s));
+ } while (nextIndex !== 0);
+ const commands = [];
+ keys.forEach((key) => {
+ commands.push(["hmget", key, "userID", "username", "connected"]);
+ });
+ return this.redisClient
+ .multi(commands)
+ .exec()
+ .then((results) => {
+ return results
+ .map(([err, session]) => (err ? undefined : mapSession(session)))
+ .filter((v) => !!v);
+ });
+ }
+}
+module.exports = {
+ InMemorySessionStore,
+ RedisSessionStore,
+};
diff --git a/examples/react-private-messaging/src/App.css b/examples/react-private-messaging/src/App.css
new file mode 100644
index 0000000000..c01137d449
--- /dev/null
+++ b/examples/react-private-messaging/src/App.css
@@ -0,0 +1,179 @@
+*,
+*:before,
+*:after {
+ box-sizing: border-box;
+}
+
+body {
+ font-family: "Roboto", Arial, sans-serif;
+ background: #141e30; /* fallback for old browsers */
+ background: -webkit-linear-gradient(
+ to right,
+ #243b55,
+ #141e30
+ ); /* Chrome 10-25, Safari 5.1-6 */
+ background: linear-gradient(
+ to right,
+ #243b55,
+ #141e30
+ ); /* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
+}
+
+.app-container {
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ min-height: 80vh;
+ min-width: 80vw;
+}
+
+/* Chat component */
+.chat-container {
+ display: grid;
+ grid-template-areas: "column1 column2";
+ grid-template-columns: 30% 70%;
+ min-width: 80vw;
+ min-height: 70vh;
+ border: 1px solid #fff;
+ border-radius: 10px;
+ background-color: #f8f3eb;
+}
+
+.user-list {
+ grid-area: column1;
+ box-shadow: 0 0 30px rgba(0, 0, 0, 0.1);
+ margin: 0 5px 0 5px;
+ padding: 0;
+ h3 {
+ margin-bottom: 10px;
+ }
+ ul {
+ list-style: none;
+ padding: 0;
+ overflow-y: auto;
+ }
+ li {
+ padding: 5px;
+ margin-right: 10px;
+ margin-bottom: 10px;
+ .name {
+ margin-right: 5px;
+ }
+ .logged-in {
+ color: green;
+ }
+ .logged-out {
+ color: black;
+ }
+ }
+ li:hover {
+ border-radius: 6px;
+ background-color: #aaa;
+ }
+ li.user.selected {
+ border-radius: 6px;
+ background-color: #aaa;
+ }
+}
+
+.chat-window {
+ grid-area: column2;
+ display: flex;
+ flex-direction: column;
+ justify-content: space-between;
+ padding: 20px;
+ overflow-y: auto;
+
+ .chat-input {
+ width: 100%;
+ border-radius: 6px;
+ border: 1px solid #fff;
+ padding: 10px;
+ box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
+ transition: 0.3s border-color;
+ }
+
+ .chat-input:hover {
+ border: 1px solid #e0e0e0;
+ }
+
+ .chat-messages {
+ width: 100%;
+
+ .message {
+ list-style: none;
+ }
+
+ .message.receive {
+ text-align: left;
+ }
+
+ .message.sent {
+ text-align: right;
+ }
+ }
+
+ li.message.received > div.wrapper {
+ margin-bottom: 20px;
+ }
+
+ li.message.received > div.wrapper > span {
+ background-color: #cfcfcf;
+ color: #000;
+ border-radius: 6px;
+ padding: 5px;
+ }
+
+ li.message.sent > div.wrapper {
+ margin-bottom: 20px;
+ }
+
+ li.message.sent > div.wrapper > span {
+ border-radius: 6px;
+ background-color: #4355ba;
+ color: #fae8e8;
+ padding: 5px;
+ }
+}
+
+/* Sign in */
+.paper-container {
+ display: flex;
+ flex-direction: column;
+ align-items: center;
+ justify-content: center;
+}
+
+.paper {
+ padding: 60px 50px;
+ border: 1px solid #fff;
+ border-radius: 10px;
+ background-color: #f8f3eb;
+}
+
+.paper-header {
+ text-align: center;
+}
+
+.formgroup-input {
+ width: 100%;
+ margin: 5px 0;
+ padding: 5px;
+ border: 1px solid #ebebeb;
+ border-radius: 6px;
+ box-shadow: 5px 1px 10px rgba(0, 0, 0, 0.1);
+ transition: 0.6s border-color;
+}
+.formgroup-input:hover {
+ border: 1px solid #aaa;
+}
+
+.formgroup-button {
+ padding: 5px;
+ border: none;
+ background-color: #3f51b5;
+ border-radius: 6px;
+ color: #fff;
+ font-weight: 600;
+ width: 100%;
+}
diff --git a/examples/react-private-messaging/src/App.jsx b/examples/react-private-messaging/src/App.jsx
new file mode 100644
index 0000000000..fec1e0d68a
--- /dev/null
+++ b/examples/react-private-messaging/src/App.jsx
@@ -0,0 +1,141 @@
+import { useEffect, useState } from "react";
+import SignIn from "./SignIn";
+import Chat from "./Chat";
+import { socket } from "./socket";
+import "./App.css";
+
+const defaultUser = {
+ isLoggedIn: false,
+ username: null,
+ userID: null,
+ connected: false,
+};
+
+function App() {
+ const [user, setUser] = useState(defaultUser);
+ const [allUsers, setAllUsers] = useState([]);
+ const [selectedUser, setSelectedUser] = useState();
+ const [currentChatMsg, setCurrentChatMsg] = useState(
+ selectedUser?.messages || []
+ );
+
+ const onSignIn = (username) => {
+ setUser({
+ ...user,
+ isLoggedIn: username.length > 0,
+ username,
+ });
+ };
+
+ useEffect(() => {
+ if (user.isLoggedIn) {
+ function getAllUsers(users) {
+ console.log(users);
+ users = users.sort((a, b) => {
+ if (a.self) return -1;
+ if (b.self) return 1;
+ if (a.username < b.username) return -1;
+ return a.username > b.username ? 1 : 0;
+ });
+ console.log(users);
+ setAllUsers(users);
+ }
+ function setSession({ sessionID, userID }) {
+ socket.auth.sessionID = sessionID;
+ socket.userID = userID;
+ setUser({
+ ...user,
+ sessionID,
+ userID,
+ });
+ localStorage.setItem("sessionID", sessionID);
+ }
+ function handleConnectError(err) {
+ if (err.message === "invalid username") {
+ setUser(defaultUser);
+ }
+ }
+ function addNewUser(user) {
+ let newList = [...allUsers];
+ newList.push(user);
+ newList = newList.sort((a, b) => {
+ if (a.self) return -1;
+ if (b.self) return 1;
+ if (a.username < b.username) return -1;
+ return a.username > b.username ? 1 : 0;
+ });
+ setAllUsers(newList);
+ }
+ function removeUser(userID) {
+ let newList = allUsers.filter((au) => au.userID !== userID);
+ setAllUsers([...newList]);
+ }
+ function updateWithMessages({ from, to, content }) {
+ for (let i = 0; i < allUsers.length; i++) {
+ const user = allUsers[i];
+ if (user.userID === from) {
+ user.messages.push({
+ from,
+ to,
+ content,
+ });
+ }
+ if (user.userID === to) {
+ user.messages.push({
+ from,
+ to,
+ content,
+ });
+ }
+ }
+
+ setAllUsers([...allUsers]);
+ if (new Set([from, to]).has(selectedUser?.userID)) {
+ setCurrentChatMsg(
+ allUsers.filter((u) => u.userID === selectedUser?.userID)[0]
+ .messages
+ );
+ }
+ }
+ if (!socket.auth) {
+ socket.auth = { username: user.username };
+ }
+ socket.connect();
+ socket.on("connect_error", handleConnectError);
+ socket.on("users", getAllUsers);
+ socket.on("session", setSession);
+ socket.on("user connected", addNewUser);
+ socket.on("user disconnected", removeUser);
+ socket.on("private message", updateWithMessages);
+ return () => {
+ socket.off("connect_error", handleConnectError);
+ socket.off("users", getAllUsers);
+ socket.off("session", setSession);
+ socket.off("user connected", addNewUser);
+ socket.off("user disconnected", removeUser);
+ socket.off("private message", updateWithMessages);
+ };
+ }
+ }, [user.isLoggedIn, allUsers.length]);
+
+ return (
+ <>
+
+ {user.isLoggedIn ? (
+
+ ) : (
+
+ )}
+
+ >
+ );
+}
+
+export default App;
diff --git a/examples/react-private-messaging/src/Chat.jsx b/examples/react-private-messaging/src/Chat.jsx
new file mode 100644
index 0000000000..56d88b58ab
--- /dev/null
+++ b/examples/react-private-messaging/src/Chat.jsx
@@ -0,0 +1,111 @@
+import "./App.css";
+import { socket } from "./socket";
+import { useState } from "react";
+
+export default function Chat({
+ currentUser,
+ users,
+ selectedUser,
+ setSelectedUser,
+ currentChat,
+ setCurrentChat,
+}) {
+ const [newMsg, setNewMsg] = useState("");
+ const handleOnSelectUser = (user) => {
+ if (user.userID !== currentUser.userID) {
+ setSelectedUser(user);
+ setCurrentChat(user.messages);
+ }
+ };
+ const handleNewMsgKeyDown = (e) => {
+ if (e.keyCode === 13) {
+ console.log("socket emit new message");
+ const message = {
+ from: currentUser.userID,
+ to: selectedUser.userID,
+ content: newMsg,
+ };
+ socket.emit("private message", message);
+ selectedUser.messages.push(message);
+ setSelectedUser(selectedUser);
+ setNewMsg("");
+ }
+ };
+ const renderWhenNoUserOrNoSelectedUser = () => {
+ let message;
+ if (users.length === 1) {
+ message = "Please wait for users to join";
+ } else if (selectedUser == null) {
+ message = "Please select a user to chat with";
+ }
+ return (
+
+
{message}
+
+ );
+ };
+
+ return (
+
+
+
Users
+
+ {users.map((u) => (
+ handleOnSelectUser(u)}
+ >
+ {u.username}
+ {u.connected ? (
+ ●
+ ) : (
+ ●
+ )}
+ {u.userID === currentUser.userID && (You) }
+
+ ))}
+
+
+
+ {users.length === 1 || selectedUser == null ? (
+ renderWhenNoUserOrNoSelectedUser()
+ ) : (
+ <>
+
+ {currentChat.length > 0 &&
+ currentChat.map((m) => (
+
+
+ {m.content}
+
+
+ ))}
+
+
setNewMsg(e.target.value)}
+ onKeyDown={handleNewMsgKeyDown}
+ />
+ >
+ )}
+
+
+ );
+}
diff --git a/examples/react-private-messaging/src/SignIn.jsx b/examples/react-private-messaging/src/SignIn.jsx
new file mode 100644
index 0000000000..4b5c2a9e70
--- /dev/null
+++ b/examples/react-private-messaging/src/SignIn.jsx
@@ -0,0 +1,91 @@
+import {
+ Avatar,
+ Box,
+ Button,
+ Checkbox,
+ Container,
+ CssBaseline,
+ FormControlLabel,
+ Grid,
+ Link,
+ Typography,
+ TextField,
+} from "@mui/material";
+import FaceIcon from "@mui/icons-material/Face";
+import { useState } from "react";
+import "./App.css";
+
+{
+ /*
+
+
+
+
+
+
+ Sign in
+
+
+ setUsername(e.target.value)}
+ />
+
+
+ Sign In
+
+
+
+ */
+}
+
+export default function SignIn({ onSignIn }) {
+ const [username, setUsername] = useState();
+ const handleSubmit = (e) => {
+ e.preventDefault();
+ if (username && username.length > 3) {
+ onSignIn(username);
+ }
+ };
+
+ return (
+
+ );
+}
diff --git a/examples/react-private-messaging/src/main.jsx b/examples/react-private-messaging/src/main.jsx
new file mode 100644
index 0000000000..569fdf2fdd
--- /dev/null
+++ b/examples/react-private-messaging/src/main.jsx
@@ -0,0 +1,9 @@
+import React from "react";
+import ReactDOM from "react-dom/client";
+import App from "./App.jsx";
+
+ReactDOM.createRoot(document.getElementById("root")).render(
+
+
+
+);
diff --git a/examples/react-private-messaging/src/socket.js b/examples/react-private-messaging/src/socket.js
new file mode 100644
index 0000000000..e7b6c920a2
--- /dev/null
+++ b/examples/react-private-messaging/src/socket.js
@@ -0,0 +1,9 @@
+import { io } from "socket.io-client";
+
+const URL =
+ process.env.NODE_ENV === "production" ? undefined : "http://localhost:3000";
+
+export const socket = io(URL, {
+ ackTimeout: 5000,
+ autoConnect: false,
+});
diff --git a/examples/react-private-messaging/vite.config.js b/examples/react-private-messaging/vite.config.js
new file mode 100644
index 0000000000..8d70a422da
--- /dev/null
+++ b/examples/react-private-messaging/vite.config.js
@@ -0,0 +1,10 @@
+import { defineConfig } from "vite";
+import react from "@vitejs/plugin-react";
+
+// https://vitejs.dev/config/
+export default defineConfig({
+ plugins: [react()],
+ server: {
+ port: 8080,
+ },
+});