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)} + /> + + + + + */ +} + +export default function SignIn({ onSignIn }) { + const [username, setUsername] = useState(); + const handleSubmit = (e) => { + e.preventDefault(); + if (username && username.length > 3) { + onSignIn(username); + } + }; + + return ( +
+
+

Sign In

+
+ setUsername(e.target.value)} + className="formgroup-input" + id="username" + name="username" + type="text" + placeholder="Username" + required + /> + + +
+
+
+ ); +} 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, + }, +});