├── images ├── , └── app_preview_image.png ├── index.vercel.js ├── repo.json ├── client ├── build │ ├── robots.txt │ ├── favicon.ico │ ├── avatars │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ ├── 12.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ └── 9.jpg │ ├── welcome-back.png │ ├── asset-manifest.json │ ├── static │ │ ├── js │ │ │ ├── runtime-main.120840cb.js │ │ │ ├── 2.0a039c31.chunk.js.LICENSE.txt │ │ │ ├── runtime-main.120840cb.js.map │ │ │ └── main.90c51a67.chunk.js │ │ └── css │ │ │ ├── main.85225e57.chunk.css │ │ │ └── main.85225e57.chunk.css.map │ └── index.html ├── public │ ├── robots.txt │ ├── avatars │ │ ├── 0.jpg │ │ ├── 1.jpg │ │ ├── 2.jpg │ │ ├── 3.jpg │ │ ├── 4.jpg │ │ ├── 5.jpg │ │ ├── 6.jpg │ │ ├── 7.jpg │ │ ├── 8.jpg │ │ ├── 9.jpg │ │ ├── 10.jpg │ │ ├── 11.jpg │ │ └── 12.jpg │ ├── favicon.ico │ ├── welcome-back.png │ └── index.html ├── README.md ├── src │ ├── components │ │ ├── LoadingScreen.jsx │ │ ├── Chat │ │ │ ├── components │ │ │ │ ├── MessageList │ │ │ │ │ ├── components │ │ │ │ │ │ ├── InfoMessage.jsx │ │ │ │ │ │ ├── NoMessages.jsx │ │ │ │ │ │ ├── MessagesLoading.jsx │ │ │ │ │ │ ├── ClockIcon.jsx │ │ │ │ │ │ ├── ReceiverMessage.jsx │ │ │ │ │ │ └── SenderMessage.jsx │ │ │ │ │ └── index.jsx │ │ │ │ ├── OnlineIndicator.jsx │ │ │ │ ├── ChatList │ │ │ │ │ ├── components │ │ │ │ │ │ ├── ChatListItem │ │ │ │ │ │ │ ├── style.css │ │ │ │ │ │ │ └── index.jsx │ │ │ │ │ │ ├── AvatarImage.jsx │ │ │ │ │ │ ├── ChatIcon.jsx │ │ │ │ │ │ └── Footer.jsx │ │ │ │ │ └── index.jsx │ │ │ │ └── TypingArea.jsx │ │ │ ├── index.jsx │ │ │ └── use-chat-handlers.js │ │ ├── Login │ │ │ ├── style.css │ │ │ └── index.jsx │ │ ├── Logo.jsx │ │ └── Navbar.jsx │ ├── index.jsx │ ├── utils.js │ ├── styles │ │ ├── style-overrides.css │ │ ├── style.css │ │ └── font-face.css │ ├── api.js │ ├── hooks.js │ ├── state.js │ └── App.jsx ├── .gitignore └── package.json ├── docs ├── YTThumbnail.png ├── screenshot000.png └── screenshot001.png ├── vercel.json ├── app.json ├── server ├── config.js ├── demo-data.js ├── redis.js ├── utils.js └── index.js ├── package.json ├── LICENSE ├── Dockerfile ├── marketplace.json ├── .gitignore └── README.md /images/,: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /index.vercel.js: -------------------------------------------------------------------------------- 1 | require('./server') -------------------------------------------------------------------------------- /repo.json: -------------------------------------------------------------------------------- 1 | { 2 | "github": "https://github.com/redis-developer/basic-redis-chat-demo-nodejs" 3 | } 4 | -------------------------------------------------------------------------------- /client/build/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /client/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /docs/YTThumbnail.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/docs/YTThumbnail.png -------------------------------------------------------------------------------- /docs/screenshot000.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/docs/screenshot000.png -------------------------------------------------------------------------------- /docs/screenshot001.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/docs/screenshot001.png -------------------------------------------------------------------------------- /client/build/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/favicon.ico -------------------------------------------------------------------------------- /client/build/avatars/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/0.jpg -------------------------------------------------------------------------------- /client/build/avatars/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/1.jpg -------------------------------------------------------------------------------- /client/build/avatars/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/10.jpg -------------------------------------------------------------------------------- /client/build/avatars/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/11.jpg -------------------------------------------------------------------------------- /client/build/avatars/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/12.jpg -------------------------------------------------------------------------------- /client/build/avatars/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/2.jpg -------------------------------------------------------------------------------- /client/build/avatars/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/3.jpg -------------------------------------------------------------------------------- /client/build/avatars/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/4.jpg -------------------------------------------------------------------------------- /client/build/avatars/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/5.jpg -------------------------------------------------------------------------------- /client/build/avatars/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/6.jpg -------------------------------------------------------------------------------- /client/build/avatars/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/7.jpg -------------------------------------------------------------------------------- /client/build/avatars/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/8.jpg -------------------------------------------------------------------------------- /client/build/avatars/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/avatars/9.jpg -------------------------------------------------------------------------------- /client/public/avatars/0.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/0.jpg -------------------------------------------------------------------------------- /client/public/avatars/1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/1.jpg -------------------------------------------------------------------------------- /client/public/avatars/2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/2.jpg -------------------------------------------------------------------------------- /client/public/avatars/3.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/3.jpg -------------------------------------------------------------------------------- /client/public/avatars/4.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/4.jpg -------------------------------------------------------------------------------- /client/public/avatars/5.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/5.jpg -------------------------------------------------------------------------------- /client/public/avatars/6.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/6.jpg -------------------------------------------------------------------------------- /client/public/avatars/7.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/7.jpg -------------------------------------------------------------------------------- /client/public/avatars/8.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/8.jpg -------------------------------------------------------------------------------- /client/public/avatars/9.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/9.jpg -------------------------------------------------------------------------------- /client/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/favicon.ico -------------------------------------------------------------------------------- /client/build/welcome-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/build/welcome-back.png -------------------------------------------------------------------------------- /client/public/avatars/10.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/10.jpg -------------------------------------------------------------------------------- /client/public/avatars/11.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/11.jpg -------------------------------------------------------------------------------- /client/public/avatars/12.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/avatars/12.jpg -------------------------------------------------------------------------------- /images/app_preview_image.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/images/app_preview_image.png -------------------------------------------------------------------------------- /client/public/welcome-back.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/main/client/public/welcome-back.png -------------------------------------------------------------------------------- /vercel.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 2, 3 | "builds": [ 4 | { 5 | "src": "./index.vercel.js", 6 | "use": "@vercel/node" 7 | } 8 | ], 9 | "routes": [ 10 | { 11 | "src": "/(.*)", 12 | "dest": "/" 13 | } 14 | ] 15 | } 16 | -------------------------------------------------------------------------------- /client/README.md: -------------------------------------------------------------------------------- 1 | # client 2 | 3 | ## Project setup 4 | 5 | ``` 6 | yarn install 7 | ``` 8 | 9 | ### Compiles and hot-reloads for development 10 | 11 | ``` 12 | yarn start 13 | ``` 14 | 15 | ### Compiles and minifies for production 16 | 17 | ``` 18 | yarn build 19 | ``` 20 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Node.JS Redis chat", 3 | "env": { 4 | "REDIS_ENDPOINT_URL": { 5 | "description": "A Redis cloud endpoint URL.", 6 | "required": true 7 | }, 8 | "REDIS_PASSWORD": { 9 | "description": "A Redis password.", 10 | "required": true 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /client/src/components/LoadingScreen.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | 4 | export function LoadingScreen() { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | } 13 | -------------------------------------------------------------------------------- /client/src/index.jsx: -------------------------------------------------------------------------------- 1 | import "bootstrap/dist/css/bootstrap.min.css"; 2 | import "./styles/style-overrides.css"; 3 | import "./styles/style.css"; 4 | import "./styles/font-face.css"; 5 | 6 | import React from "react"; 7 | import ReactDOM from "react-dom"; 8 | 9 | import App from "./App"; 10 | 11 | ReactDOM.render(, document.getElementById("root")); 12 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/InfoMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const InfoMessage = ({ message }) => { 3 | return ( 4 |

8 | {message} 9 |

10 | ); 11 | }; 12 | 13 | export default InfoMessage; 14 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/OnlineIndicator.jsx: -------------------------------------------------------------------------------- 1 | const OnlineIndicator = ({ online, hide = false, width = 8, height = 8 }) => { 2 | return ( 3 |
9 | ); 10 | }; 11 | 12 | export default OnlineIndicator; 13 | -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /server/config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | /** Get default port argument. */ 4 | let DEFAULT_PORT = 4000; 5 | try { 6 | const newPort = parseInt(process.argv[2]); 7 | DEFAULT_PORT = isNaN(newPort) ? DEFAULT_PORT : newPort; 8 | } catch (e) { 9 | } 10 | 11 | const PORT = process.env.PORT || DEFAULT_PORT; 12 | 13 | const ipAddress = require('ip').address(); 14 | 15 | const SERVER_ID = `${ipAddress}:${PORT}`; 16 | 17 | module.exports = { PORT, SERVER_ID }; 18 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/NoMessages.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import { CardText } from "react-bootstrap-icons"; 4 | 5 | const NoMessages = () => { 6 | return ( 7 |
8 | 9 |

No messages

10 |
11 | ); 12 | }; 13 | 14 | export default NoMessages; 15 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/MessagesLoading.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | 4 | const MessagesLoading = () => { 5 | return ( 6 |
7 |
8 | 9 |
10 |
11 | ); 12 | }; 13 | 14 | export default MessagesLoading; 15 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/ChatListItem/style.css: -------------------------------------------------------------------------------- 1 | .chat-list-item { 2 | cursor: pointer; 3 | padding: 14px 16px; 4 | } 5 | .mdi-circle:before { 6 | content: "󰝥"; 7 | } 8 | 9 | .mdi-set, 10 | .mdi:before { 11 | display: inline-block; 12 | font: normal normal normal 24px/1 Material Design Icons; 13 | font-size: inherit; 14 | text-rendering: auto; 15 | line-height: inherit; 16 | -webkit-font-smoothing: antialiased; 17 | -moz-osx-font-smoothing: grayscale; 18 | } 19 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/ClockIcon.jsx: -------------------------------------------------------------------------------- 1 | const ClockIcon = () => ( 2 | 12 | ); 13 | 14 | export default ClockIcon; 15 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | Node.JS Redis chat 13 | 14 | 15 | 16 |
17 | 18 | 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-redis-socketio-chat", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "license": "MIT", 6 | "dependencies": { 7 | "@types/redis": "^2.8.28", 8 | "bcrypt": "^5.0.0", 9 | "body-parser": "^1.19.0", 10 | "connect-redis": "^5.0.0", 11 | "dotenv": "^8.2.0", 12 | "express": "^4.17.1", 13 | "express-session": "^1.17.1", 14 | "ip": "^1.1.5", 15 | "moment": "^2.29.1", 16 | "node-random-name": "^1.0.1", 17 | "redis": "^3.0.2", 18 | "socket.io": "^3.0.4" 19 | }, 20 | "scripts": { 21 | "dev": "nodemon server/index.js", 22 | "start": "node server/index.js" 23 | }, 24 | "devDependencies": { 25 | "@types/socket.io": "^2.1.12", 26 | "nodemon": "^2.0.6" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.21.1", 7 | "bootstrap": "^4.5.3", 8 | "jdenticon": "^3.1.0", 9 | "moment": "^2.29.1", 10 | "react": "^17.0.1", 11 | "react-bootstrap": "^1.4.0", 12 | "react-bootstrap-icons": "^1.1.0", 13 | "react-dom": "^17.0.1", 14 | "react-scripts": "4.0.1", 15 | "socket.io-client": "^3.0.4" 16 | }, 17 | "scripts": { 18 | "start": "react-scripts start", 19 | "build": "react-scripts build" 20 | }, 21 | "eslintConfig": { 22 | "extends": [ 23 | "react-app", 24 | "react-app/jest" 25 | ] 26 | }, 27 | "proxy": "http://localhost:4000", 28 | "browserslist": { 29 | "production": [ 30 | ">0.2%", 31 | "not dead", 32 | "not op_mini all" 33 | ], 34 | "development": [ 35 | "last 1 chrome version", 36 | "last 1 firefox version", 37 | "last 1 safari version" 38 | ] 39 | }, 40 | "devDependencies": { 41 | "@types/socket.io-client": "^1.4.34" 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Redis Developer 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /client/src/components/Chat/components/TypingArea.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const TypingArea = ({ message, setMessage, onSubmit }) => ( 3 |
4 |
5 |
6 |
7 | setMessage(e.target.value)} 10 | type="text" 11 | placeholder="Enter Message..." 12 | className="form-control chat-input" 13 | /> 14 | {/**/} 15 |
16 |
17 |
18 | 27 |
28 |
29 |
30 | ); 31 | 32 | export default TypingArea; 33 | -------------------------------------------------------------------------------- /client/build/asset-manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": { 3 | "main.css": "/static/css/main.85225e57.chunk.css", 4 | "main.js": "/static/js/main.90c51a67.chunk.js", 5 | "main.js.map": "/static/js/main.90c51a67.chunk.js.map", 6 | "runtime-main.js": "/static/js/runtime-main.120840cb.js", 7 | "runtime-main.js.map": "/static/js/runtime-main.120840cb.js.map", 8 | "static/css/2.1a02f21c.chunk.css": "/static/css/2.1a02f21c.chunk.css", 9 | "static/js/2.0a039c31.chunk.js": "/static/js/2.0a039c31.chunk.js", 10 | "static/js/2.0a039c31.chunk.js.map": "/static/js/2.0a039c31.chunk.js.map", 11 | "index.html": "/index.html", 12 | "static/css/2.1a02f21c.chunk.css.map": "/static/css/2.1a02f21c.chunk.css.map", 13 | "static/css/main.85225e57.chunk.css.map": "/static/css/main.85225e57.chunk.css.map", 14 | "static/js/2.0a039c31.chunk.js.LICENSE.txt": "/static/js/2.0a039c31.chunk.js.LICENSE.txt" 15 | }, 16 | "entrypoints": [ 17 | "static/js/runtime-main.120840cb.js", 18 | "static/css/2.1a02f21c.chunk.css", 19 | "static/js/2.0a039c31.chunk.js", 20 | "static/css/main.85225e57.chunk.css", 21 | "static/js/main.90c51a67.chunk.js" 22 | ] 23 | } -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/ReceiverMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import moment from "moment"; 3 | import React from "react"; 4 | import ClockIcon from "./ClockIcon"; 5 | 6 | const ReceiverMessage = ({ 7 | username = "user", 8 | message = "Lorem ipsum dolor...", 9 | date, 10 | }) => ( 11 |
12 |
13 |
14 |
18 |
19 |
25 | {username} 26 |
27 |

{message}

28 |

29 | {moment.unix(date).format("LT")}{" "} 30 |

31 |
32 |
33 |
34 |
35 | ); 36 | export default ReceiverMessage; 37 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/AvatarImage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useMemo } from "react"; 3 | import { getAvatarByUserAndRoomId } from "../../../../../utils"; 4 | import ChatIcon from "./ChatIcon"; 5 | 6 | const AvatarImage = ({ name, id }) => { 7 | const url = useMemo(() => { 8 | const av = getAvatarByUserAndRoomId("" + id); 9 | if (name === "Mary") { 10 | return `${process.env.PUBLIC_URL}/avatars/0.jpg`; 11 | } else if (name === "Pablo") { 12 | return `${process.env.PUBLIC_URL}/avatars/2.jpg`; 13 | } else if (name === "Joe") { 14 | return `${process.env.PUBLIC_URL}/avatars/9.jpg`; 15 | } else if (name === "Alex") { 16 | return `${process.env.PUBLIC_URL}/avatars/8.jpg`; 17 | } 18 | return av; 19 | }, [id, name]); 20 | 21 | return ( 22 | <> 23 | {name !== "General" ? ( 24 | {name} 30 | ) : ( 31 |
32 | 33 |
34 | )} 35 | 36 | ); 37 | }; 38 | 39 | export default AvatarImage; 40 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/ChatIcon.jsx: -------------------------------------------------------------------------------- 1 | const ChatIcon = () => ( 2 | 9 | 10 | 15 | 16 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | ); 31 | 32 | export default ChatIcon; 33 | -------------------------------------------------------------------------------- /client/build/static/js/runtime-main.120840cb.js: -------------------------------------------------------------------------------- 1 | !function(e){function t(t){for(var n,l,i=t[0],f=t[1],a=t[2],p=0,s=[];p { 7 | const processedRooms = useMemo(() => { 8 | const roomsList = Object.values(rooms); 9 | const main = roomsList.filter((x) => x.id === "0"); 10 | let other = roomsList.filter((x) => x.id !== "0"); 11 | other = other.sort( 12 | (a, b) => +a.id.split(":").pop() - +b.id.split(":").pop() 13 | ); 14 | return [...(main ? main : []), ...other]; 15 | }, [rooms]); 16 | return ( 17 | <> 18 |
19 |
20 |

Chats

21 |
22 |
23 |
24 | {processedRooms.map((room) => ( 25 | 28 | dispatch({ type: "set current room", payload: room.id }) 29 | } 30 | active={currentRoom === room.id} 31 | room={room} 32 | /> 33 | ))} 34 |
35 |
36 |
37 |
38 | 39 | ); 40 | }; 41 | 42 | export default ChatList; 43 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/components/SenderMessage.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import moment from "moment"; 3 | import React from "react"; 4 | import ClockIcon from "./ClockIcon"; 5 | import OnlineIndicator from "../../OnlineIndicator"; 6 | 7 | const SenderMessage = ({ 8 | user, 9 | message = "Lorem ipsum dolor...", 10 | date, 11 | onUserClicked, 12 | }) => ( 13 |
14 |
15 |
19 |
20 | {user && ( 21 |
22 |
30 | {user.username} 31 |
32 | 33 |
34 | )} 35 |

{message}

36 |

37 | {moment.unix(date).format("LT")}{" "} 38 |

39 |
40 |
41 |
42 |
43 |
44 | ); 45 | 46 | export default SenderMessage; 47 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | # Copyright 2020 Google LLC 2 | # 3 | # Licensed under the Apache License, Version 2.0 (the "License"); 4 | # you may not use this file except in compliance with the License. 5 | # You may obtain a copy of the License at 6 | # 7 | # https://www.apache.org/licenses/LICENSE-2.0 8 | # 9 | # Unless required by applicable law or agreed to in writing, software 10 | # distributed under the License is distributed on an "AS IS" BASIS, 11 | # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 | # See the License for the specific language governing permissions and 13 | # limitations under the License. 14 | 15 | # [START cloudrun_helloworld_dockerfile] 16 | # [START run_helloworld_dockerfile] 17 | 18 | # Use the official lightweight Node.js 12 image. 19 | # https://hub.docker.com/_/node 20 | FROM node:12-slim 21 | 22 | # Create and change to the app directory. 23 | WORKDIR /usr/src/app 24 | 25 | # Copy application dependency manifests to the container image. 26 | # A wildcard is used to ensure copying both package.json AND package-lock.json (when available). 27 | # Copying this first prevents re-running npm install on every code change. 28 | COPY package*.json ./ 29 | 30 | # Install production dependencies. 31 | # If you add a package-lock.json, speed your build by switching to 'npm ci'. 32 | # RUN npm ci --only=production 33 | RUN npm install --only=production 34 | 35 | # Copy local code to the container image. 36 | COPY . ./ 37 | 38 | # Run the web service on container startup. 39 | CMD [ "node", "server/index.js" ] 40 | 41 | # [END run_helloworld_dockerfile] 42 | # [END cloudrun_helloworld_dockerfile] -------------------------------------------------------------------------------- /client/build/static/js/2.0a039c31.chunk.js.LICENSE.txt: -------------------------------------------------------------------------------- 1 | /* 2 | object-assign 3 | (c) Sindre Sorhus 4 | @license MIT 5 | */ 6 | 7 | /*! 8 | Copyright (c) 2017 Jed Watson. 9 | Licensed under the MIT License (MIT), see 10 | http://jedwatson.github.io/classnames 11 | */ 12 | 13 | /*! 14 | * The buffer module from node.js, for the browser. 15 | * 16 | * @author Feross Aboukhadijeh 17 | * @license MIT 18 | */ 19 | 20 | /*! ieee754. BSD-3-Clause License. Feross Aboukhadijeh */ 21 | 22 | /** @license React v0.20.1 23 | * scheduler.production.min.js 24 | * 25 | * Copyright (c) Facebook, Inc. and its affiliates. 26 | * 27 | * This source code is licensed under the MIT license found in the 28 | * LICENSE file in the root directory of this source tree. 29 | */ 30 | 31 | /** @license React v17.0.1 32 | * react-dom.production.min.js 33 | * 34 | * Copyright (c) Facebook, Inc. and its affiliates. 35 | * 36 | * This source code is licensed under the MIT license found in the 37 | * LICENSE file in the root directory of this source tree. 38 | */ 39 | 40 | /** @license React v17.0.1 41 | * react-jsx-runtime.production.min.js 42 | * 43 | * Copyright (c) Facebook, Inc. and its affiliates. 44 | * 45 | * This source code is licensed under the MIT license found in the 46 | * LICENSE file in the root directory of this source tree. 47 | */ 48 | 49 | /** @license React v17.0.1 50 | * react.production.min.js 51 | * 52 | * Copyright (c) Facebook, Inc. and its affiliates. 53 | * 54 | * This source code is licensed under the MIT license found in the 55 | * LICENSE file in the root directory of this source tree. 56 | */ 57 | 58 | //! moment.js 59 | -------------------------------------------------------------------------------- /marketplace.json: -------------------------------------------------------------------------------- 1 | { 2 | "app_name": "Basic Redis chat app in Nodejs", 3 | "description": "Showcases how to impliment chat app in Node.js, Socket.IO and Redis", 4 | "type": "App", 5 | "contributed_by": "Redis", 6 | "repo_url": "https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs", 7 | "preview_image_url": "https://raw.githubusercontent.com/redis-developer/basic-redis-chat-app-demo-nodejs/master/images/app_preview_image.png", 8 | "download_url": "https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs/archive/main.zip", 9 | "hosted_url": "", 10 | "quick_deploy": "true", 11 | "deploy_buttons": [ 12 | { 13 | "heroku": "https://heroku.com/deploy?template=https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs" 14 | }, 15 | { 16 | "Google": "https://deploy.cloud.run/?git_repo=https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs.git" 17 | } 18 | ], 19 | "language": [ 20 | "JavaScript" 21 | ], 22 | "redis_commands": [ 23 | "AUTH", 24 | "INCR", 25 | "DECR", 26 | "HMSET", 27 | "EXISTS", 28 | "HEXISTS", 29 | "SET", 30 | "GET", 31 | "HGETALL", 32 | "ZRANGEBYSCORE", 33 | "ZADD", 34 | "SADD", 35 | "HMGET", 36 | "SISMEMBER", 37 | "SMEMBERS", 38 | "SREM", 39 | "PUBLISH", 40 | "SUBSCRIBE" 41 | ], 42 | "redis_use_cases": [ 43 | "Pub/Sub" 44 | ], 45 | "redis_features": [], 46 | "app_image_urls": [ 47 | "https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs/raw/main/docs/screenshot001.png" 48 | ], 49 | "youtube_url": "https://www.youtube.com/watch?v=miK7xDkDXF0", 50 | "special_tags": [], 51 | "verticals": [], 52 | "markdown": "https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs/raw/main/README.md" 53 | } -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/Footer.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import React from "react"; 4 | import { Power } from "react-bootstrap-icons"; 5 | import OnlineIndicator from "../../OnlineIndicator"; 6 | import AvatarImage from "./AvatarImage"; 7 | 8 | const Footer = ({ user, onLogOut }) => ( 9 |
13 | {true ? ( 14 | <> 15 | 16 | 17 | 18 | ) : ( 19 | <> 20 | 21 | 22 | 23 | )} 24 |
25 | ); 26 | 27 | const LogoutButton = ({ onLogOut, col = 5, noinfo = false }) => ( 28 |
33 | Log out 34 |
35 | ); 36 | 37 | const UserInfo = ({ user, col = 7, noinfo = false }) => ( 38 |
43 |
44 | 45 |
46 | {!noinfo && ( 47 |
48 |
{user.username}
49 |
50 | 51 |

Active

52 |
53 |
54 | )} 55 |
56 | ); 57 | 58 | export default Footer; 59 | -------------------------------------------------------------------------------- /client/src/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import { getUsers } from "./api"; 4 | 5 | /** 6 | * @param {string[]} names 7 | * @param {string} username 8 | */ 9 | export const parseRoomName = (names, username) => { 10 | for (let name of names) { 11 | if (typeof name !== 'string') { 12 | name = name[0]; 13 | } 14 | if (name !== username) { 15 | return name; 16 | } 17 | } 18 | return names[0]; 19 | }; 20 | 21 | /** Get an avatar for a room or a user */ 22 | export const getAvatarByUserAndRoomId = (roomId = "1") => { 23 | const TOTAL_IMAGES = 13; 24 | const seed1 = 654; 25 | const seed2 = 531; 26 | 27 | const uidParsed = +roomId.split(":").pop(); 28 | let roomIdParsed = +roomId.split(":").reverse().pop(); 29 | if (roomIdParsed < 0) { 30 | roomIdParsed += 3555; 31 | } 32 | 33 | const theId = (uidParsed * seed1 + roomIdParsed * seed2) % TOTAL_IMAGES; 34 | 35 | return `${process.env.PUBLIC_URL}/avatars/${theId}.jpg`; 36 | }; 37 | 38 | const jdenticon = require("jdenticon"); 39 | 40 | const avatars = {}; 41 | export const getAvatar = (username) => { 42 | let av = avatars[username]; 43 | if (av === undefined) { 44 | av = 45 | "data:image/svg+xml;base64," + window.btoa(jdenticon.toSvg(username, 50)); 46 | avatars[username] = av; 47 | } 48 | return av; 49 | }; 50 | 51 | export const populateUsersFromLoadedMessages = async (users, dispatch, messages) => { 52 | const userIds = {}; 53 | messages.forEach((message) => { 54 | userIds[message.from] = 1; 55 | }); 56 | 57 | const ids = Object.keys(userIds).filter( 58 | (id) => users[id] === undefined 59 | ); 60 | 61 | if (ids.length !== 0) { 62 | /** We need to fetch users first */ 63 | const newUsers = await getUsers(ids); 64 | dispatch({ 65 | type: "append users", 66 | payload: newUsers, 67 | }); 68 | } 69 | 70 | }; -------------------------------------------------------------------------------- /client/build/index.html: -------------------------------------------------------------------------------- 1 | Node.JS Redis chat
-------------------------------------------------------------------------------- /client/src/styles/style-overrides.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --primary: rgb(85, 110, 230) !important; 3 | --light: #f5f5f8 !important; 4 | --success: rgb(52, 195, 143) !important; 5 | } 6 | 7 | .bg-success { 8 | background-color: var(--success) !important; 9 | } 10 | 11 | .bg-light { 12 | background-color: var(--light) !important; 13 | } 14 | 15 | .bg-gray { 16 | background-color: var(--gray) !important; 17 | } 18 | 19 | .bg-primary { 20 | background-color: var(--primary) !important; 21 | } 22 | 23 | .text-primary { 24 | color: var(--primary) !important; 25 | } 26 | 27 | .list-group-item.active { 28 | background-color: var(--primary) !important; 29 | border-color: var(--primary) !important; 30 | } 31 | 32 | .btn-rounded { 33 | border-radius: 30px !important; 34 | } 35 | 36 | .btn { 37 | display: inline-block; 38 | font-weight: 400; 39 | color: #495057; 40 | text-align: center; 41 | vertical-align: middle; 42 | user-select: none; 43 | background-color: transparent; 44 | border: 1px solid transparent; 45 | border-radius: 30px !important; 46 | padding: 0.47rem 0.75rem; 47 | font-size: 0.8125rem; 48 | line-height: 1.5; 49 | border-radius: 0.25rem; 50 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 51 | border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out; 52 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 53 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 54 | transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, 55 | border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, 56 | -webkit-box-shadow 0.15s ease-in-out; 57 | } 58 | 59 | .btn-primary { 60 | color: #fff; 61 | background-color: var(--primary); 62 | border-color: var(--primary); 63 | } 64 | 65 | .btn-primary.focus, 66 | .btn-primary:focus, 67 | .btn-primary:hover { 68 | color: #fff; 69 | background-color: #3452e1; 70 | border-color: #2948df; 71 | } 72 | 73 | .font-size-14 { 74 | font-size: 14px !important; 75 | } 76 | 77 | .font-size-11 { 78 | font-size: 11px !important; 79 | } 80 | 81 | .font-size-12 { 82 | font-size: 12px !important; 83 | } 84 | 85 | .font-size-15 { 86 | font-size: 15px !important; 87 | } 88 | 89 | .w-md { 90 | min-width: 110px; 91 | } 92 | -------------------------------------------------------------------------------- /client/src/components/Login/style.css: -------------------------------------------------------------------------------- 1 | .login-form .username-select button { 2 | background-color: transparent !important; 3 | color: inherit; 4 | padding: 7.5px 12px !important; 5 | display: block !important; 6 | border: 1px solid rgb(206, 212, 218) !important; 7 | border-radius: 4px !important; 8 | width: 100% !important; 9 | text-align: left !important; 10 | } 11 | 12 | .login-form .username-select .btn-primary:not(:disabled):not(.disabled).active, 13 | .login-form .username-select .btn-primary:not(:disabled):not(.disabled):active, 14 | .show > .btn-primary.dropdown-toggle { 15 | color: inherit; 16 | } 17 | 18 | .login-form .username-select .dropdown-menu.show.dropdown-menu-right { 19 | transform: translate(0px, 38px) !important; 20 | } 21 | 22 | .username-select-dropdown { 23 | position: relative; 24 | display: flex !important; 25 | align-items: center; 26 | background-color: transparent !important; 27 | color: inherit; 28 | padding: 0 12px !important; 29 | border: 1px solid rgb(206, 212, 218) !important; 30 | border-radius: 4px !important; 31 | width: 100% !important; 32 | text-align: left !important; 33 | height: calc(1.5em + 0.94rem + 2px) !important; 34 | 35 | cursor: pointer; 36 | } 37 | 38 | .username-select-dropdown .username-select-block { 39 | background-color: var(--white); 40 | position: absolute; 41 | top: -1138px; 42 | left: 0; 43 | opacity: 0; 44 | transform: scale(0.5, 0.5); 45 | transform-origin: top left; 46 | transition: opacity 0.2s ease, transform 0.2s ease; 47 | 48 | border: 1px solid rgb(206, 212, 218) !important; 49 | border-radius: 4px !important; 50 | 51 | padding: 8px 0px; 52 | } 53 | 54 | .username-select-dropdown:focus { 55 | outline: none; 56 | border-color: #80bdff !important; 57 | box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25); 58 | } 59 | 60 | .username-select-dropdown .username-select-block.open { 61 | top: 42px; 62 | transform: scale(1, 1); 63 | opacity: 1; 64 | } 65 | 66 | .username-select-row { 67 | display: flex; 68 | width: 100%; 69 | justify-content: space-between; 70 | align-items: center; 71 | } 72 | 73 | .username-select-dropdown .username-select-block .username-select-block-item { 74 | padding: 4px 24px; 75 | } 76 | 77 | .username-select-dropdown 78 | .username-select-block 79 | .username-select-block-item:hover { 80 | background-color: var(--light); 81 | } 82 | -------------------------------------------------------------------------------- /client/src/components/Logo.jsx: -------------------------------------------------------------------------------- 1 | const Logo = ({ width = 64, height = 64 }) => { 2 | return ( 3 | 10 | 11 | 12 | 16 | 20 | 21 | 22 | 23 | 27 | 28 | 29 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 43 | 44 | 45 | ); 46 | }; 47 | 48 | export default Logo; 49 | -------------------------------------------------------------------------------- /client/src/components/Chat/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import ChatList from "./components/ChatList"; 4 | import MessageList from "./components/MessageList"; 5 | import TypingArea from "./components/TypingArea"; 6 | import useChatHandlers from "./use-chat-handlers"; 7 | 8 | /** 9 | * @param {{ 10 | * onLogOut: () => void, 11 | * onMessageSend: (message: string, roomId: string) => void, 12 | * user: import("../../state").UserEntry 13 | * }} props 14 | */ 15 | export default function Chat({ onLogOut, user, onMessageSend }) { 16 | const { 17 | onLoadMoreMessages, 18 | onUserClicked, 19 | message, 20 | setMessage, 21 | rooms, 22 | room, 23 | currentRoom, 24 | dispatch, 25 | messageListElement, 26 | roomId, 27 | messages, 28 | users, 29 | } = useChatHandlers(user); 30 | 31 | return ( 32 |
33 |
34 |
35 | 42 |
43 | {/* Chat Box*/} 44 |
45 |
46 |

{room ? room.name : "Room"}

47 |
48 | 57 | 58 | {/* Typing area */} 59 | { 63 | e.preventDefault(); 64 | onMessageSend(message.trim(), roomId); 65 | setMessage(""); 66 | 67 | messageListElement.current.scrollTop = 68 | messageListElement.current.scrollHeight; 69 | }} 70 | /> 71 |
72 |
73 |
74 | ); 75 | } 76 | -------------------------------------------------------------------------------- /client/src/components/Navbar.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useEffect, useState } from "react"; 3 | import { getButtonLinks } from "../api"; 4 | 5 | const Navbar = () => { 6 | /** 7 | * @type {[{ 8 | * heroku?: string; 9 | * google_cloud?: string; 10 | * vercel?: string; 11 | * github?: string; 12 | * }, React.Dispatch]} 13 | */ 14 | const [links, setLinks] = useState(null); 15 | useEffect(() => { 16 | getButtonLinks().then(setLinks); 17 | }, []); 18 | return ( 19 | 29 | ); 30 | }; 31 | 32 | const GithubIcon = ({ link }) => ( 33 | 39 | 47 | 52 | 57 | 58 | 59 | ); 60 | 61 | export default Navbar; 62 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # Snowpack dependency directory (https://snowpack.dev/) 45 | web_modules/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | .parcel-cache 78 | 79 | # Next.js build output 80 | .next 81 | out 82 | 83 | # Nuxt.js build / generate output 84 | .nuxt 85 | dist 86 | 87 | # Gatsby files 88 | .cache/ 89 | # Comment in the public line in if your project uses Gatsby and not Next.js 90 | # https://nextjs.org/blog/next-9-1#public-directory-support 91 | # public 92 | 93 | # vuepress build output 94 | .vuepress/dist 95 | 96 | # Serverless directories 97 | .serverless/ 98 | 99 | # FuseBox cache 100 | .fusebox/ 101 | 102 | # DynamoDB Local files 103 | .dynamodb/ 104 | 105 | # TernJS port file 106 | .tern-port 107 | 108 | # Stores VSCode versions used for testing VSCode extensions 109 | .vscode-test 110 | 111 | # yarn v2 112 | .yarn/cache 113 | .yarn/unplugged 114 | .yarn/build-state.yml 115 | .yarn/install-state.gz 116 | .pnp.* 117 | 118 | 119 | .env 120 | .env.backup 121 | 122 | !client/build 123 | -------------------------------------------------------------------------------- /server/demo-data.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const moment = require('moment'); 3 | const { zadd } = require('./redis'); 4 | const { createUser, createPrivateRoom, getPrivateRoomId } = require('./utils'); 5 | /** Creating demo data */ 6 | const demoPassword = 'password123'; 7 | 8 | const demoUsers = ["Pablo", "Joe", "Mary", 'Alex']; 9 | 10 | const greetings = ["Hello", "Hi", "Yo", "Hola"]; 11 | 12 | const messages = [ 13 | 'Hello!', 14 | 'Hi, How are you? What about our next meeting?', 15 | 'Yeah everything is fine', 16 | 'Next meeting tomorrow 10.00AM', 17 | `Wow that's great` 18 | ]; 19 | 20 | const getGreeting = () => greetings[Math.floor(Math.random() * greetings.length)]; 21 | 22 | const addMessage = async (roomId, fromId, content, timestamp = moment().unix()) => { 23 | const roomKey = `room:${roomId}`; 24 | 25 | const message = { 26 | from: fromId, 27 | date: timestamp, 28 | message: content, 29 | roomId, 30 | }; 31 | /** Now the other user sends the greeting to the user */ 32 | await zadd(roomKey, "" + message.date, JSON.stringify(message)); 33 | }; 34 | 35 | const createDemoData = async () => { 36 | /** For each name create a user. */ 37 | const users = []; 38 | for (let x = 0; x < demoUsers.length; x++) { 39 | const user = await createUser(demoUsers[x], demoPassword); 40 | /** This one should go to the session */ 41 | users.push(user); 42 | } 43 | 44 | const rooms = {}; 45 | /** Once the demo users were created, for each user send messages to other ones. */ 46 | for (let userIndex = 0; userIndex < users.length; userIndex++) { 47 | const user = users[userIndex]; 48 | const otherUsers = users.filter(x => x.id !== user.id); 49 | 50 | for (let otherUserIndex = 0; otherUserIndex < otherUsers.length; otherUserIndex++) { 51 | const otherUser = otherUsers[otherUserIndex]; 52 | let privateRoomId = getPrivateRoomId(user.id, otherUser.id); 53 | let room = rooms[privateRoomId]; 54 | if (room === undefined) { 55 | const res = await createPrivateRoom(user.id, otherUser.id); 56 | room = res[0]; 57 | rooms[privateRoomId] = room; 58 | } 59 | 60 | await addMessage(privateRoomId, otherUser.id, getGreeting(), moment().unix() - Math.random() * 222); 61 | } 62 | } 63 | const randomUserId = () => users[Math.floor(users.length * Math.random())].id; 64 | for (let messageIndex = 0; messageIndex < messages.length; messageIndex++) { 65 | await addMessage('0', randomUserId(), messages[messageIndex], moment().unix() - ((messages.length - messageIndex) * 200)); 66 | } 67 | }; 68 | 69 | module.exports = { 70 | createDemoData 71 | }; 72 | -------------------------------------------------------------------------------- /server/redis.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | require("dotenv").config(); 3 | const redis = require("redis"); 4 | 5 | const endpoint = process.env.REDIS_ENDPOINT_URL || "127.0.0.1:6379"; 6 | const password = process.env.REDIS_PASSWORD || null; 7 | 8 | const [host, port] = endpoint.split(":"); 9 | 10 | const resolvePromise = (resolve, reject) => { 11 | return (err, data) => { 12 | if (err) { 13 | reject(err); 14 | } 15 | resolve(data); 16 | }; 17 | }; 18 | 19 | const auth = (client) => new Promise((a, b) => { 20 | if (password === null) { 21 | a(true); 22 | } else { 23 | client.auth(password, resolvePromise(a, b)); 24 | } 25 | }); 26 | 27 | /** @type {import('redis').RedisClient} */ 28 | const client = redis.createClient(+port, host); 29 | 30 | /** @type {import('redis').RedisClient} */ 31 | const sub = redis.createClient(+port, host, password === null ? undefined : { 32 | password 33 | }); 34 | 35 | module.exports = { 36 | client, 37 | sub, 38 | auth: async () => { 39 | await auth(client); 40 | await auth(sub); 41 | }, 42 | incr: (key = "key") => 43 | new Promise((a, b) => client.incr(key, resolvePromise(a, b))), 44 | decr: (key = "key") => 45 | new Promise((a, b) => client.decr(key, resolvePromise(a, b))), 46 | hmset: (key = "key", values = []) => 47 | new Promise((a, b) => client.hmset(key, values, resolvePromise(a, b))), 48 | exists: (key = "key") => 49 | new Promise((a, b) => client.exists(key, resolvePromise(a, b))), 50 | hexists: (key = "key", key2 = "") => 51 | new Promise((a, b) => client.hexists(key, key2, resolvePromise(a, b))), 52 | set: (key = "key", value) => 53 | new Promise((a, b) => client.set(key, value, resolvePromise(a, b))), 54 | get: (key = "key") => 55 | new Promise((a, b) => client.get(key, resolvePromise(a, b))), 56 | hgetall: (key = "key") => 57 | new Promise((a, b) => client.hgetall(key, resolvePromise(a, b))), 58 | zrangebyscore: (key = "key", min = 0, max = 1) => 59 | new Promise((a, b) => 60 | client.zrangebyscore(key, min, max, resolvePromise(a, b)) 61 | ), 62 | zadd: (key = "key", key2 = "", value) => 63 | new Promise((a, b) => client.zadd(key, key2, value, resolvePromise(a, b))), 64 | sadd: (key = "key", value) => 65 | new Promise((a, b) => client.sadd(key, value, resolvePromise(a, b))), 66 | hmget: (key = "key", key2 = "") => 67 | new Promise((a, b) => client.hmget(key, key2, resolvePromise(a, b))), 68 | sismember: (key = "key", key2 = "") => 69 | new Promise((a, b) => client.sismember(key, key2, resolvePromise(a, b))), 70 | smembers: (key = "key") => 71 | new Promise((a, b) => client.smembers(key, resolvePromise(a, b))), 72 | srem: (key = "key", key2 = "") => 73 | new Promise((a, b) => client.srem(key, key2, resolvePromise(a, b))), 74 | }; 75 | -------------------------------------------------------------------------------- /client/src/api.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | axios.defaults.withCredentials = true; 3 | 4 | const BASE_URL = ''; 5 | 6 | export const MESSAGES_TO_LOAD = 15; 7 | 8 | const url = x => `${BASE_URL}${x}`; 9 | 10 | /** Checks if there's an existing session. */ 11 | export const getMe = () => { 12 | return axios.get(url('/me')) 13 | .then(x => x.data) 14 | .catch(_ => null); 15 | }; 16 | 17 | /** Handle user log in */ 18 | export const login = (username, password) => { 19 | return axios.post(url('/login'), { 20 | username, 21 | password 22 | }).then(x => 23 | x.data 24 | ) 25 | .catch(e => { throw new Error(e.response && e.response.data && e.response.data.message); }); 26 | }; 27 | 28 | export const logOut = () => { 29 | return axios.post(url('/logout')); 30 | }; 31 | 32 | /** 33 | * Function for checking which deployment urls exist. 34 | * 35 | * @returns {Promise<{ 36 | * heroku?: string; 37 | * google_cloud?: string; 38 | * vercel?: string; 39 | * github?: string; 40 | * }>} 41 | */ 42 | export const getButtonLinks = () => { 43 | return axios.get(url('/links')) 44 | .then(x => x.data) 45 | .catch(_ => null); 46 | }; 47 | 48 | /** This was used to get a random login name (for demo purposes). */ 49 | export const getRandomName = () => { 50 | return axios.get(url('/randomname')).then(x => x.data); 51 | }; 52 | 53 | /** 54 | * Load messages 55 | * 56 | * @param {string} id room id 57 | * @param {number} offset 58 | * @param {number} size 59 | */ 60 | export const getMessages = (id, 61 | offset = 0, 62 | size = MESSAGES_TO_LOAD 63 | ) => { 64 | return axios.get(url(`/room/${id}/messages`), { 65 | params: { 66 | offset, 67 | size 68 | } 69 | }) 70 | .then(x => x.data.reverse()); 71 | }; 72 | 73 | /** 74 | * @returns {Promise<{ name: string, id: string, messages: Array }>} 75 | */ 76 | export const getPreloadedRoom = async () => { 77 | return axios.get(url(`/room/0/preload`)).then(x => x.data); 78 | }; 79 | 80 | /** 81 | * Fetch users by requested ids 82 | * @param {Array} ids 83 | */ 84 | export const getUsers = (ids) => { 85 | return axios.get(url(`/users`), { params: { ids } }).then(x => x.data); 86 | }; 87 | 88 | /** Fetch users which are online */ 89 | export const getOnlineUsers = () => { 90 | return axios.get(url(`/users/online`)).then(x => x.data); 91 | }; 92 | 93 | /** This one is called on a private messages room created. */ 94 | export const addRoom = async (user1, user2) => { 95 | return axios.post(url(`/room`), { user1, user2 }).then(x => x.data); 96 | }; 97 | 98 | /** 99 | * @returns {Promise>} 100 | */ 101 | export const getRooms = async (userId) => { 102 | return axios.get(url(`/rooms/${userId}`)).then(x => x.data); 103 | }; 104 | -------------------------------------------------------------------------------- /client/src/components/Chat/components/MessageList/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React from "react"; 3 | import { MESSAGES_TO_LOAD } from "../../../../api"; 4 | import InfoMessage from "./components/InfoMessage"; 5 | import MessagesLoading from "./components/MessagesLoading"; 6 | import NoMessages from "./components/NoMessages"; 7 | import ReceiverMessage from "./components/ReceiverMessage"; 8 | import SenderMessage from "./components/SenderMessage"; 9 | 10 | const MessageList = ({ 11 | messageListElement, 12 | messages, 13 | room, 14 | onLoadMoreMessages, 15 | user, 16 | onUserClicked, 17 | users, 18 | }) => ( 19 |
23 | {messages === undefined ? ( 24 | 25 | ) : messages.length === 0 ? ( 26 | 27 | ) : ( 28 | <> 29 | )} 30 |
31 | {messages && messages.length !== 0 && ( 32 | <> 33 | {room.offset && room.offset >= MESSAGES_TO_LOAD ? ( 34 |
35 |
38 |
39 | 49 |
50 |
53 |
54 | ) : ( 55 | <> 56 | )} 57 | {messages.map((message, x) => { 58 | const key = message.message + message.date + message.from + x; 59 | if (message.from === "info") { 60 | return ; 61 | } 62 | if (+message.from !== +user.id) { 63 | return ( 64 | onUserClicked(message.from)} 66 | key={key} 67 | message={message.message} 68 | date={message.date} 69 | user={users[message.from]} 70 | /> 71 | ); 72 | } 73 | return ( 74 | 82 | ); 83 | })} 84 | 85 | )} 86 |
87 |
88 | ); 89 | export default MessageList; 90 | -------------------------------------------------------------------------------- /server/utils.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const bcrypt = require('bcrypt'); 3 | const { incr, set, hmset, sadd, hmget, exists, 4 | client: redisClient, 5 | } = require('./redis'); 6 | 7 | /** Redis key for the username (for getting the user id) */ 8 | const makeUsernameKey = (username) => { 9 | const usernameKey = `username:${username}`; 10 | return usernameKey; 11 | }; 12 | 13 | /** 14 | * Creates a user and adds default chat rooms 15 | * @param {string} username 16 | * @param {string} password 17 | */ 18 | const createUser = async (username, password) => { 19 | const usernameKey = makeUsernameKey(username); 20 | /** Create user */ 21 | const hashedPassword = await bcrypt.hash(password, 10); 22 | const nextId = await incr("total_users"); 23 | const userKey = `user:${nextId}`; 24 | await set(usernameKey, userKey); 25 | await hmset(userKey, ["username", username, "password", hashedPassword]); 26 | 27 | /** 28 | * Each user has a set of rooms he is in 29 | * let's define the default ones 30 | */ 31 | await sadd(`user:${nextId}:rooms`, `${0}`); // Main room 32 | 33 | /** This one should go to the session */ 34 | return { id: nextId, username }; 35 | }; 36 | 37 | const getPrivateRoomId = (user1, user2) => { 38 | if (isNaN(user1) || isNaN(user2) || user1 === user2) { 39 | return null; 40 | } 41 | const minUserId = user1 > user2 ? user2 : user1; 42 | const maxUserId = user1 > user2 ? user1 : user2; 43 | return `${minUserId}:${maxUserId}`; 44 | }; 45 | 46 | /** 47 | * Create a private room and add users to it 48 | * @returns {Promise<[{ 49 | * id: string; 50 | * names: any[]; 51 | * }, boolean]>} 52 | */ 53 | const createPrivateRoom = async (user1, user2) => { 54 | const roomId = getPrivateRoomId(user1, user2); 55 | 56 | if (roomId === null) { 57 | return [null, true]; 58 | } 59 | 60 | /** Add rooms to those users */ 61 | await sadd(`user:${user1}:rooms`, `${roomId}`); 62 | await sadd(`user:${user2}:rooms`, `${roomId}`); 63 | 64 | return [{ 65 | id: roomId, 66 | names: [ 67 | await hmget(`user:${user1}`, "username"), 68 | await hmget(`user:${user2}`, "username"), 69 | ], 70 | }, false]; 71 | }; 72 | 73 | 74 | const getMessages = async (roomId = "0", offset = 0, size = 50) => { 75 | /** 76 | * Logic: 77 | * 1. Check if room with id exists 78 | * 2. Fetch messages from last hour 79 | **/ 80 | const roomKey = `room:${roomId}`; 81 | const roomExists = await exists(roomKey); 82 | if (!roomExists) { 83 | return []; 84 | } else { 85 | return new Promise((resolve, reject) => { 86 | redisClient.zrevrange(roomKey, offset, offset + size, (err, values) => { 87 | if (err) { 88 | reject(err); 89 | } 90 | resolve(values.map((val) => JSON.parse(val))); 91 | }); 92 | }); 93 | } 94 | }; 95 | 96 | const sanitise = (text) => { 97 | let sanitisedText = text; 98 | 99 | if (text.indexOf('<') > -1 || text.indexOf('>') > -1) { 100 | sanitisedText = text.replace(//g, '>'); 101 | } 102 | 103 | return sanitisedText; 104 | }; 105 | 106 | module.exports = { 107 | getMessages, 108 | sanitise, 109 | createUser, 110 | makeUsernameKey, 111 | createPrivateRoom, 112 | getPrivateRoomId 113 | }; -------------------------------------------------------------------------------- /client/src/components/Chat/use-chat-handlers.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useCallback } from "react"; 3 | import { useEffect, useState, useRef } from "react"; 4 | import { addRoom, getMessages } from "../../api"; 5 | import { useAppState } from "../../state"; 6 | import { parseRoomName, populateUsersFromLoadedMessages } from "../../utils"; 7 | 8 | /** Lifecycle hooks with callbacks for the Chat component */ 9 | const useChatHandlers = (/** @type {import("../../state").UserEntry} */ user) => { 10 | const [state, dispatch] = useAppState(); 11 | const messageListElement = useRef(null); 12 | 13 | /** @type {import("../../state").Room} */ 14 | const room = state.rooms[state.currentRoom]; 15 | const roomId = room?.id; 16 | const messages = room?.messages; 17 | 18 | const [message, setMessage] = useState(""); 19 | 20 | const scrollToTop = useCallback(() => { 21 | setTimeout(() => { 22 | if (messageListElement.current) { 23 | messageListElement.current.scrollTop = 0; 24 | } 25 | }, 0); 26 | }, []); 27 | 28 | const scrollToBottom = useCallback(() => { 29 | if (messageListElement.current) { 30 | messageListElement.current.scrollTo({ 31 | top: messageListElement.current.scrollHeight, 32 | }); 33 | } 34 | }, []); 35 | 36 | useEffect(() => { 37 | scrollToBottom(); 38 | }, [messages, scrollToBottom]); 39 | 40 | const onFetchMessages = useCallback( 41 | (offset = 0, prepend = false) => { 42 | getMessages(roomId, offset).then(async (messages) => { 43 | /** We've got messages but it's possible we might not have the cached user entires which correspond to those messages */ 44 | await populateUsersFromLoadedMessages(state.users, dispatch, messages); 45 | 46 | dispatch({ 47 | type: prepend ? "prepend messages" : "set messages", 48 | payload: { id: roomId, messages: messages }, 49 | }); 50 | if (prepend) { 51 | setTimeout(() => { 52 | scrollToTop(); 53 | }, 10); 54 | } else { 55 | scrollToBottom(); 56 | } 57 | }); 58 | }, 59 | [dispatch, roomId, scrollToBottom, scrollToTop, state.users] 60 | ); 61 | 62 | useEffect(() => { 63 | if (roomId === undefined) { 64 | return; 65 | } 66 | if (messages === undefined) { 67 | /** Fetch logic goes here */ 68 | onFetchMessages(); 69 | } 70 | }, [ 71 | messages, 72 | dispatch, 73 | roomId, 74 | state.users, 75 | state, 76 | scrollToBottom, 77 | onFetchMessages, 78 | ]); 79 | 80 | useEffect(() => { 81 | if (messageListElement.current) { 82 | scrollToBottom(); 83 | } 84 | }, [scrollToBottom, roomId]); 85 | 86 | const onUserClicked = async (userId) => { 87 | /** Check if room exists. */ 88 | const targetUser = state.users[userId]; 89 | let roomId = targetUser.room; 90 | if (roomId === undefined) { 91 | // @ts-ignore 92 | const room = await addRoom(userId, user.id); 93 | roomId = room.id; 94 | /** We need to set this room id to user. */ 95 | dispatch({ type: "set user", payload: { ...targetUser, room: roomId } }); 96 | /** Then a new room should be added to the store. */ 97 | dispatch({ 98 | type: "add room", 99 | // @ts-ignore 100 | payload: { id: roomId, name: parseRoomName(room.names, user.username) }, 101 | }); 102 | } 103 | /** Then a room should be changed */ 104 | dispatch({ type: "set current room", payload: roomId }); 105 | }; 106 | 107 | const onLoadMoreMessages = useCallback(() => { 108 | onFetchMessages(room.offset, true); 109 | }, [onFetchMessages, room]); 110 | 111 | return { 112 | onLoadMoreMessages, 113 | onUserClicked, 114 | message, 115 | setMessage, 116 | dispatch, 117 | room, 118 | rooms: state.rooms, 119 | currentRoom: state.currentRoom, 120 | messageListElement, 121 | roomId, 122 | users: state.users, 123 | messages, 124 | }; 125 | }; 126 | export default useChatHandlers; -------------------------------------------------------------------------------- /client/src/components/Chat/components/ChatList/components/ChatListItem/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import "./style.css"; 3 | import React, { useMemo } from "react"; 4 | import { useAppState } from "../../../../../../state"; 5 | import moment from "moment"; 6 | import { useEffect } from "react"; 7 | import { getMessages } from "../../../../../../api"; 8 | import AvatarImage from "../AvatarImage"; 9 | import OnlineIndicator from "../../../OnlineIndicator"; 10 | 11 | /** 12 | * @param {{ active: boolean; room: import('../../../../../../state').Room; onClick: () => void; }} props 13 | */ 14 | const ChatListItem = ({ room, active = false, onClick }) => { 15 | const { online, name, lastMessage, userId } = useChatListItemHandlers(room); 16 | return ( 17 |
23 |
24 | 25 |
26 |
27 | 28 |
29 |
30 |
{name}
31 | {lastMessage && ( 32 |

{lastMessage.message}

33 | )} 34 |
35 | {lastMessage && ( 36 |
37 | {moment.unix(lastMessage.date).format("LT")} 38 |
39 | )} 40 |
41 | ); 42 | }; 43 | 44 | const useChatListItemHandlers = ( 45 | /** @type {import("../../../../../../state").Room} */ room 46 | ) => { 47 | const { id, name } = room; 48 | const [state] = useAppState(); 49 | 50 | /** Here we want to associate the room with a user by its name (since it's unique). */ 51 | const [isUser, online, userId] = useMemo(() => { 52 | try { 53 | let pseudoUserId = Math.abs(parseInt(id.split(":").reverse().pop())); 54 | const isUser = pseudoUserId > 0; 55 | const usersFiltered = Object.entries(state.users) 56 | .filter(([, user]) => user.username === name) 57 | .map(([, user]) => user); 58 | let online = false; 59 | if (usersFiltered.length > 0) { 60 | online = usersFiltered[0].online; 61 | pseudoUserId = +usersFiltered[0].id; 62 | } 63 | return [isUser, online, pseudoUserId]; 64 | } catch (_) { 65 | return [false, false, "0"]; 66 | } 67 | }, [id, name, state.users]); 68 | 69 | const lastMessage = useLastMessage(room); 70 | 71 | return { 72 | isUser, 73 | online, 74 | userId, 75 | name: room.name, 76 | lastMessage, 77 | }; 78 | }; 79 | 80 | const useLastMessage = ( 81 | /** @type {import("../../../../../../state").Room} */ room 82 | ) => { 83 | const [, dispatch] = useAppState(); 84 | const { lastMessage } = room; 85 | useEffect(() => { 86 | if (lastMessage === undefined) { 87 | /** need to fetch it */ 88 | if (room.messages === undefined) { 89 | getMessages(room.id, 0, 1).then((messages) => { 90 | let message = null; 91 | if (messages.length !== 0) { 92 | message = messages.pop(); 93 | } 94 | dispatch({ 95 | type: "set last message", 96 | payload: { id: room.id, lastMessage: message }, 97 | }); 98 | }); 99 | } else if (room.messages.length === 0) { 100 | dispatch({ 101 | type: "set last message", 102 | payload: { id: room.id, lastMessage: null }, 103 | }); 104 | } else { 105 | dispatch({ 106 | type: "set last message", 107 | payload: { 108 | id: room.id, 109 | lastMessage: room.messages[room.messages.length - 1], 110 | }, 111 | }); 112 | } 113 | } 114 | }, [lastMessage, dispatch, room]); 115 | 116 | return lastMessage; 117 | }; 118 | 119 | export default ChatListItem; 120 | -------------------------------------------------------------------------------- /client/src/hooks.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { useEffect, useRef, useState } from "react"; 3 | import { getMe, login, logOut } from "./api"; 4 | import io from "socket.io-client"; 5 | import { parseRoomName } from "./utils"; 6 | 7 | /** 8 | * @param {import('./state').UserEntry} newUser 9 | */ 10 | const updateUser = (newUser, dispatch, infoMessage) => { 11 | dispatch({ type: "set user", payload: newUser }); 12 | if (infoMessage !== undefined) { 13 | dispatch({ 14 | type: "append message", 15 | payload: { 16 | id: "0", 17 | message: { 18 | /** Date isn't shown in the info message, so we only need a unique value */ 19 | date: Math.random() * 10000, 20 | from: "info", 21 | message: infoMessage, 22 | }, 23 | }, 24 | }); 25 | } 26 | }; 27 | 28 | /** @returns {[SocketIOClient.Socket, boolean]} */ 29 | const useSocket = (user, dispatch) => { 30 | const [connected, setConnected] = useState(false); 31 | /** @type {React.MutableRefObject} */ 32 | const socketRef = useRef(null); 33 | const socket = socketRef.current; 34 | 35 | /** First of all it's necessary to handle the socket io connection */ 36 | useEffect(() => { 37 | if (user === null) { 38 | if (socket !== null) { 39 | socket.disconnect(); 40 | } 41 | setConnected(false); 42 | } else { 43 | if (socket !== null) { 44 | socket.connect(); 45 | } else { 46 | socketRef.current = io(); 47 | } 48 | setConnected(true); 49 | } 50 | }, [user, socket]); 51 | 52 | /** 53 | * Once we are sure the socket io object is initialized 54 | * Add event listeners. 55 | */ 56 | useEffect(() => { 57 | if (connected && user) { 58 | socket.on("user.connected", (newUser) => { 59 | updateUser(newUser, dispatch, `${newUser.username} connected`); 60 | }); 61 | socket.on("user.disconnected", (newUser) => 62 | updateUser(newUser, dispatch, `${newUser.username} left`) 63 | ); 64 | socket.on("show.room", (room) => { 65 | console.log({ user }); 66 | dispatch({ 67 | type: "add room", 68 | payload: { 69 | id: room.id, 70 | name: parseRoomName(room.names, user.username), 71 | }, 72 | }); 73 | }); 74 | socket.on("message", (message) => { 75 | /** Set user online */ 76 | dispatch({ 77 | type: "make user online", 78 | payload: message.from, 79 | }); 80 | dispatch({ 81 | type: "append message", 82 | payload: { id: message.roomId === undefined ? "0" : message.roomId, message }, 83 | }); 84 | }); 85 | } else { 86 | /** If there was a log out, we need to clear existing listeners on an active socket connection */ 87 | if (socket) { 88 | socket.off("user.connected"); 89 | socket.off("user.disconnected"); 90 | socket.off("user.room"); 91 | socket.off("message"); 92 | } 93 | } 94 | }, [connected, user, dispatch, socket]); 95 | 96 | return [socket, connected]; 97 | }; 98 | 99 | /** User management hook. */ 100 | const useUser = (onUserLoaded = (user) => { }, dispatch) => { 101 | const [loading, setLoading] = useState(true); 102 | /** @type {[import('./state.js').UserEntry | null, React.Dispatch]} */ 103 | const [user, setUser] = useState(null); 104 | /** Callback used in log in form. */ 105 | const onLogIn = ( 106 | username = "", 107 | password = "", 108 | onError = (val = null) => { }, 109 | onLoading = (loading = false) => { } 110 | ) => { 111 | onError(null); 112 | onLoading(true); 113 | login(username, password) 114 | .then((x) => { 115 | setUser(x); 116 | }) 117 | .catch((e) => onError(e.message)) 118 | .finally(() => onLoading(false)); 119 | }; 120 | 121 | /** Log out form */ 122 | const onLogOut = async () => { 123 | logOut().then(() => { 124 | setUser(null); 125 | /** This will clear the store, to completely re-initialize an app on the next login. */ 126 | dispatch({ type: "clear" }); 127 | setLoading(true); 128 | }); 129 | }; 130 | 131 | /** Runs once when the component is mounted to check if there's user stored in cookies */ 132 | useEffect(() => { 133 | if (!loading) { 134 | return; 135 | } 136 | getMe().then((user) => { 137 | setUser(user); 138 | setLoading(false); 139 | onUserLoaded(user); 140 | }); 141 | }, [onUserLoaded, loading]); 142 | 143 | return { user, onLogIn, onLogOut, loading }; 144 | }; 145 | 146 | export { 147 | updateUser, 148 | useSocket, 149 | useUser 150 | }; -------------------------------------------------------------------------------- /client/src/state.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { createContext, useContext, useReducer } from "react"; 3 | 4 | /** 5 | * @typedef {{ 6 | * from: string 7 | * date: number 8 | * message: string 9 | * roomId?: string 10 | * }} Message 11 | * 12 | * @typedef {{ 13 | * name: string; 14 | * id: string; 15 | * messages?: Message[] 16 | * connected?: boolean; 17 | * offset?: number; 18 | * forUserId?: null | number | string 19 | * lastMessage?: Message | null 20 | * }} Room 21 | * 22 | * @typedef {{ 23 | * username: string; 24 | * id: string; 25 | * online?: boolean; 26 | * room?: string; 27 | * }} UserEntry 28 | * 29 | * @typedef {{ 30 | * currentRoom: string; 31 | * rooms: {[id: string]: Room}; 32 | * users: {[id: string]: UserEntry} 33 | * }} State 34 | * 35 | * @param {State} state 36 | * @param {{type: string; payload: any}} action 37 | * @returns {State} 38 | */ 39 | const reducer = (state, action) => { 40 | switch (action.type) { 41 | case "clear": 42 | return { currentRoom: "0", rooms: {}, users: {} }; 43 | case "set user": { 44 | return { 45 | ...state, 46 | users: { ...state.users, [action.payload.id]: action.payload }, 47 | }; 48 | } 49 | case "make user online": { 50 | return { 51 | ...state, 52 | users: { 53 | ...state.users, 54 | [action.payload]: { ...state.users[action.payload], online: true }, 55 | }, 56 | }; 57 | } 58 | case "append users": { 59 | return { ...state, users: { ...state.users, ...action.payload } }; 60 | } 61 | case "set messages": { 62 | return { 63 | ...state, 64 | rooms: { 65 | ...state.rooms, 66 | [action.payload.id]: { 67 | ...state.rooms[action.payload.id], 68 | messages: action.payload.messages, 69 | offset: action.payload.messages.length, 70 | }, 71 | }, 72 | }; 73 | } 74 | case "prepend messages": { 75 | const messages = [ 76 | ...action.payload.messages, 77 | ...state.rooms[action.payload.id].messages, 78 | ]; 79 | return { 80 | ...state, 81 | rooms: { 82 | ...state.rooms, 83 | [action.payload.id]: { 84 | ...state.rooms[action.payload.id], 85 | messages, 86 | offset: messages.length, 87 | }, 88 | }, 89 | }; 90 | } 91 | case "append message": 92 | if (state.rooms[action.payload.id] === undefined) { 93 | return state; 94 | } 95 | return { 96 | ...state, 97 | rooms: { 98 | ...state.rooms, 99 | [action.payload.id]: { 100 | ...state.rooms[action.payload.id], 101 | lastMessage: action.payload.message, 102 | messages: state.rooms[action.payload.id].messages 103 | ? [ 104 | ...state.rooms[action.payload.id].messages, 105 | action.payload.message, 106 | ] 107 | : undefined, 108 | }, 109 | }, 110 | }; 111 | case 'set last message': 112 | return { ...state, rooms: { ...state.rooms, [action.payload.id]: { ...state.rooms[action.payload.id], lastMessage: action.payload.lastMessage } } }; 113 | case "set current room": 114 | return { ...state, currentRoom: action.payload }; 115 | case "add room": 116 | return { 117 | ...state, 118 | rooms: { ...state.rooms, [action.payload.id]: action.payload }, 119 | }; 120 | case "set rooms": { 121 | /** @type {Room[]} */ 122 | const newRooms = action.payload; 123 | const rooms = { ...state.rooms }; 124 | newRooms.forEach((room) => { 125 | rooms[room.id] = { 126 | ...room, 127 | messages: rooms[room.id] && rooms[room.id].messages, 128 | }; 129 | }); 130 | return { ...state, rooms }; 131 | } 132 | default: 133 | return state; 134 | } 135 | }; 136 | 137 | /** @type {State} */ 138 | const initialState = { 139 | currentRoom: "main", 140 | rooms: {}, 141 | users: {}, 142 | }; 143 | 144 | const useAppStateContext = () => { 145 | return useReducer(reducer, initialState); 146 | }; 147 | 148 | // @ts-ignore 149 | export const AppContext = createContext(); 150 | 151 | /** 152 | * @returns {[ 153 | * State, 154 | * React.Dispatch<{ 155 | * type: string; 156 | * payload: any; 157 | * }> 158 | * ]} 159 | */ 160 | export const useAppState = () => { 161 | const [state, dispatch] = useContext(AppContext); 162 | return [state, dispatch]; 163 | }; 164 | 165 | export default useAppStateContext; -------------------------------------------------------------------------------- /client/src/styles/style.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: "Poppins", Arial, Helvetica, sans-serif; 3 | font-size: 13px; 4 | color: #495057; 5 | } 6 | 7 | .navbar { 8 | box-shadow: rgba(18, 38, 63, 0.03) 0px 12px 24px 0px; 9 | } 10 | 11 | .navbar-brand { 12 | font-size: 16px; 13 | } 14 | 15 | .chats-title { 16 | padding-left: 14px; 17 | } 18 | 19 | .login-page { 20 | display: flex; 21 | align-items: center; 22 | flex-direction: column; 23 | justify-content: center; 24 | padding-bottom: 190px; 25 | height: 100vh; 26 | } 27 | 28 | .form-signin { 29 | width: 100%; 30 | max-width: 330px; 31 | padding: 15px; 32 | margin: 0 auto; 33 | } 34 | 35 | .text-small { 36 | font-size: 0.9rem; 37 | } 38 | 39 | .messages-box, 40 | .chat-box { 41 | /* height: 510px; */ 42 | width: 100%; 43 | } 44 | 45 | .chat-box-wrapper { 46 | flex: 1; 47 | overflow-y: scroll; 48 | } 49 | 50 | .rounded-lg { 51 | border-radius: 0.5rem; 52 | } 53 | 54 | input::placeholder { 55 | font-size: 0.9rem; 56 | color: #999; 57 | } 58 | 59 | .centered-box { 60 | width: 100%; 61 | height: 100vh; 62 | display: flex; 63 | align-items: center; 64 | justify-content: center; 65 | } 66 | 67 | .login-error-anchor { 68 | position: relative; 69 | } 70 | 71 | .toast-box { 72 | text-align: left; 73 | margin-top: 30px; 74 | position: absolute; 75 | width: 100%; 76 | top: 0; 77 | display: flex; 78 | flex-direction: row; 79 | justify-content: center; 80 | } 81 | 82 | .full-height { 83 | height: 100vh; 84 | flex-direction: column; 85 | display: flex; 86 | } 87 | 88 | .full-height .container { 89 | flex: 1; 90 | } 91 | 92 | .container .row { 93 | height: 100%; 94 | } 95 | 96 | .flex-column { 97 | display: flex; 98 | flex-direction: column; 99 | } 100 | 101 | .bg-white.flex-column { 102 | height: 100%; 103 | } 104 | 105 | .flex { 106 | flex: 1; 107 | } 108 | 109 | .logout-button { 110 | cursor: pointer; 111 | display: flex; 112 | flex-direction: row; 113 | align-items: center; 114 | padding: 15px 20px; 115 | } 116 | 117 | .logout-button svg { 118 | margin-right: 15px; 119 | } 120 | 121 | .no-messages { 122 | opacity: 0.5; 123 | height: 100%; 124 | width: 100%; 125 | } 126 | 127 | .avatar-box { 128 | width: 50px; 129 | height: 50px; 130 | object-fit: cover; 131 | object-position: 50%; 132 | overflow: hidden; 133 | border-radius: 4px; 134 | cursor: pointer; 135 | } 136 | 137 | .user-link { 138 | cursor: pointer; 139 | } 140 | 141 | .user-link:hover { 142 | text-decoration: underline; 143 | } 144 | 145 | .online-indicator { 146 | width: 14px; 147 | height: 14px; 148 | border: 2px solid white; 149 | bottom: -7px; 150 | right: -7px; 151 | background-color: #4df573; 152 | } 153 | 154 | .online-indicator.selected { 155 | border: none; 156 | width: 12px; 157 | height: 12px; 158 | bottom: -5px; 159 | right: -5px; 160 | } 161 | 162 | .online-indicator.offline { 163 | background-color: #bbb; 164 | } 165 | 166 | span.pseudo-link { 167 | font-size: 14px; 168 | text-decoration: underline; 169 | color: var(--blue); 170 | cursor: pointer; 171 | } 172 | 173 | span.pseudo-link:hover { 174 | text-decoration: none; 175 | } 176 | 177 | .list-group-item { 178 | cursor: pointer; 179 | height: 70px; 180 | box-sizing: border-box; 181 | transition: background-color 0.1s ease-out; 182 | } 183 | 184 | .chat-icon { 185 | width: 45px; 186 | height: 45px; 187 | border-radius: 4px; 188 | background-color: #eee; 189 | } 190 | 191 | .chat-icon.active { 192 | background-color: var(--blue); 193 | } 194 | 195 | .chats-title { 196 | font-size: 15px; 197 | } 198 | 199 | .chat-body { 200 | border-radius: 10px !important; 201 | } 202 | 203 | .chat-list-container { 204 | height: 100%; 205 | } 206 | 207 | .chat-input { 208 | border-radius: 30px !important; 209 | background-color: #eff2f7 !important; 210 | border-color: #eff2f7 !important; 211 | padding-right: 120px; 212 | } 213 | 214 | .form-control::placeholder { 215 | font-size: 13px; 216 | } 217 | 218 | .form-control { 219 | display: block; 220 | width: 100%; 221 | height: calc(1.5em + 0.94rem + 2px); 222 | padding: 7.5px 12px; 223 | font-size: 13px; 224 | font-weight: 400; 225 | line-height: 1.5; 226 | color: #495057; 227 | background-color: #fff; 228 | background-clip: padding-box; 229 | border: 1px solid #ced4da; 230 | -webkit-transition: border-color 0.15s ease-in-out, 231 | -webkit-box-shadow 0.15s ease-in-out; 232 | transition: border-color 0.15s ease-in-out, 233 | -webkit-box-shadow 0.15s ease-in-out; 234 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out; 235 | transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out, 236 | -webkit-box-shadow 0.15s ease-in-out; 237 | } 238 | 239 | .rounded-button { 240 | border-radius: 30px; 241 | background-color: var(--light); 242 | } 243 | -------------------------------------------------------------------------------- /client/src/App.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import React, { useEffect, useCallback } from "react"; 3 | import Login from "./components/Login"; 4 | import Chat from "./components/Chat"; 5 | import { getOnlineUsers, getRooms } from "./api"; 6 | import useAppStateContext, { AppContext } from "./state"; 7 | import moment from "moment"; 8 | import { parseRoomName } from "./utils"; 9 | import { LoadingScreen } from "./components/LoadingScreen"; 10 | import Navbar from "./components/Navbar"; 11 | import { useSocket, useUser } from "./hooks"; 12 | 13 | const App = () => { 14 | const { 15 | loading, 16 | user, 17 | state, 18 | dispatch, 19 | onLogIn, 20 | onMessageSend, 21 | onLogOut, 22 | } = useAppHandlers(); 23 | 24 | if (loading) { 25 | return ; 26 | } 27 | 28 | const showLogin = !user; 29 | 30 | return ( 31 | 32 |
38 | 39 | {showLogin ? ( 40 | 41 | ) : ( 42 | 43 | )} 44 |
45 |
46 | ); 47 | }; 48 | 49 | const useAppHandlers = () => { 50 | const [state, dispatch] = useAppStateContext(); 51 | const onUserLoaded = useCallback( 52 | (user) => { 53 | if (user !== null) { 54 | if (!state.users[user.id]) { 55 | dispatch({ type: "set user", payload: { ...user, online: true } }); 56 | } 57 | } 58 | }, 59 | [dispatch, state.users] 60 | ); 61 | 62 | const { user, onLogIn, onLogOut, loading } = useUser(onUserLoaded, dispatch); 63 | const [socket, connected] = useSocket(user, dispatch); 64 | 65 | /** Socket joins specific rooms once they are added */ 66 | useEffect(() => { 67 | if (user === null) { 68 | /** We are logged out */ 69 | /** But it's necessary to pre-populate the main room, so the user won't wait for messages once he's logged in */ 70 | return; 71 | } 72 | if (connected) { 73 | /** 74 | * The socket needs to be joined to the newly added rooms 75 | * on an active connection. 76 | */ 77 | const newRooms = []; 78 | Object.keys(state.rooms).forEach((roomId) => { 79 | const room = state.rooms[roomId]; 80 | if (room.connected) { 81 | return; 82 | } 83 | newRooms.push({ ...room, connected: true }); 84 | socket.emit("room.join", room.id); 85 | }); 86 | if (newRooms.length !== 0) { 87 | dispatch({ type: "set rooms", payload: newRooms }); 88 | } 89 | } else { 90 | /** 91 | * It's necessary to set disconnected flags on rooms 92 | * once the client is not connected 93 | */ 94 | const newRooms = []; 95 | Object.keys(state.rooms).forEach((roomId) => { 96 | const room = state.rooms[roomId]; 97 | if (!room.connected) { 98 | return; 99 | } 100 | newRooms.push({ ...room, connected: false }); 101 | }); 102 | /** Only update the state if it's only necessary */ 103 | if (newRooms.length !== 0) { 104 | dispatch({ type: "set rooms", payload: newRooms }); 105 | } 106 | } 107 | }, [user, connected, dispatch, socket, state.rooms, state.users]); 108 | 109 | /** Populate default rooms when user is not null */ 110 | useEffect(() => { 111 | if (Object.values(state.rooms).length === 0 && user !== null) { 112 | /** First of all fetch online users. */ 113 | getOnlineUsers().then((users) => { 114 | dispatch({ 115 | type: "append users", 116 | payload: users, 117 | }); 118 | }); 119 | /** Then get rooms. */ 120 | getRooms(user.id).then((rooms) => { 121 | const payload = []; 122 | rooms.forEach(({ id, names }) => { 123 | payload.push({ id, name: parseRoomName(names, user.username) }); 124 | }); 125 | /** Here we also can populate the state with default chat rooms */ 126 | dispatch({ 127 | type: "set rooms", 128 | payload, 129 | }); 130 | dispatch({ type: "set current room", payload: "0" }); 131 | }); 132 | } 133 | }, [dispatch, state.rooms, user]); 134 | 135 | const onMessageSend = useCallback( 136 | (message, roomId) => { 137 | if (typeof message !== "string" || message.trim().length === 0) { 138 | return; 139 | } 140 | if (!socket) { 141 | /** Normally there shouldn't be such case. */ 142 | console.error("Couldn't send message"); 143 | } 144 | socket.emit("message", { 145 | roomId: roomId, 146 | message, 147 | from: user.id, 148 | date: moment(new Date()).unix(), 149 | }); 150 | }, 151 | [user, socket] 152 | ); 153 | 154 | return { 155 | loading, 156 | user, 157 | state, 158 | dispatch, 159 | onLogIn, 160 | onMessageSend, 161 | onLogOut, 162 | }; 163 | }; 164 | 165 | export default App; 166 | -------------------------------------------------------------------------------- /client/src/styles/font-face.css: -------------------------------------------------------------------------------- 1 | /* devanagari */ 2 | @font-face { 3 | font-family: "Poppins"; 4 | font-style: normal; 5 | font-weight: 300; 6 | font-display: swap; 7 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2) 8 | format("woff2"); 9 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 10 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 11 | } 12 | /* latin-ext */ 13 | @font-face { 14 | font-family: "Poppins"; 15 | font-style: normal; 16 | font-weight: 300; 17 | font-display: swap; 18 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2) 19 | format("woff2"); 20 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 21 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 22 | } 23 | /* latin */ 24 | @font-face { 25 | font-family: "Poppins"; 26 | font-style: normal; 27 | font-weight: 300; 28 | font-display: swap; 29 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2) 30 | format("woff2"); 31 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 32 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 33 | U+FEFF, U+FFFD; 34 | } 35 | /* devanagari */ 36 | @font-face { 37 | font-family: "Poppins"; 38 | font-style: normal; 39 | font-weight: 400; 40 | font-display: swap; 41 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) 42 | format("woff2"); 43 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 44 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 45 | } 46 | /* latin-ext */ 47 | @font-face { 48 | font-family: "Poppins"; 49 | font-style: normal; 50 | font-weight: 400; 51 | font-display: swap; 52 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) 53 | format("woff2"); 54 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 55 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 56 | } 57 | /* latin */ 58 | @font-face { 59 | font-family: "Poppins"; 60 | font-style: normal; 61 | font-weight: 400; 62 | font-display: swap; 63 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) 64 | format("woff2"); 65 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 66 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 67 | U+FEFF, U+FFFD; 68 | } 69 | /* devanagari */ 70 | @font-face { 71 | font-family: "Poppins"; 72 | font-style: normal; 73 | font-weight: 500; 74 | font-display: swap; 75 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2) 76 | format("woff2"); 77 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 78 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 79 | } 80 | /* latin-ext */ 81 | @font-face { 82 | font-family: "Poppins"; 83 | font-style: normal; 84 | font-weight: 500; 85 | font-display: swap; 86 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2) 87 | format("woff2"); 88 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 89 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 90 | } 91 | /* latin */ 92 | @font-face { 93 | font-family: "Poppins"; 94 | font-style: normal; 95 | font-weight: 500; 96 | font-display: swap; 97 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2) 98 | format("woff2"); 99 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 100 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 101 | U+FEFF, U+FFFD; 102 | } 103 | /* devanagari */ 104 | @font-face { 105 | font-family: "Poppins"; 106 | font-style: normal; 107 | font-weight: 600; 108 | font-display: swap; 109 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2) 110 | format("woff2"); 111 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 112 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 113 | } 114 | /* latin-ext */ 115 | @font-face { 116 | font-family: "Poppins"; 117 | font-style: normal; 118 | font-weight: 600; 119 | font-display: swap; 120 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2) 121 | format("woff2"); 122 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 123 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 124 | } 125 | /* latin */ 126 | @font-face { 127 | font-family: "Poppins"; 128 | font-style: normal; 129 | font-weight: 600; 130 | font-display: swap; 131 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2) 132 | format("woff2"); 133 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 134 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 135 | U+FEFF, U+FFFD; 136 | } 137 | /* devanagari */ 138 | @font-face { 139 | font-family: "Poppins"; 140 | font-style: normal; 141 | font-weight: 700; 142 | font-display: swap; 143 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2) 144 | format("woff2"); 145 | unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8, 146 | U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB; 147 | } 148 | /* latin-ext */ 149 | @font-face { 150 | font-family: "Poppins"; 151 | font-style: normal; 152 | font-weight: 700; 153 | font-display: swap; 154 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2) 155 | format("woff2"); 156 | unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB, 157 | U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF; 158 | } 159 | /* latin */ 160 | @font-face { 161 | font-family: "Poppins"; 162 | font-style: normal; 163 | font-weight: 700; 164 | font-display: swap; 165 | src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2) 166 | format("woff2"); 167 | unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA, 168 | U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215, 169 | U+FEFF, U+FFFD; 170 | } 171 | -------------------------------------------------------------------------------- /client/src/components/Login/index.jsx: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | import { Toast } from "react-bootstrap"; 3 | import React, { useState, useRef } from "react"; 4 | import Logo from "../Logo"; 5 | import "./style.css"; 6 | import { useEffect } from "react"; 7 | 8 | const DEMO_USERS = ["Pablo", "Joe", "Mary", "Alex"]; 9 | 10 | export default function Login({ onLogIn }) { 11 | const [username, setUsername] = useState( 12 | () => DEMO_USERS[Math.floor(Math.random() * DEMO_USERS.length)] 13 | ); 14 | const [password, setPassword] = useState("password123"); 15 | const [error, setError] = useState(null); 16 | 17 | const onSubmit = async (event) => { 18 | event.preventDefault(); 19 | onLogIn(username, password, setError); 20 | }; 21 | 22 | return ( 23 | <> 24 |
25 |
31 |
32 |
43 |
44 |

Welcome Back !

45 |

Sign in to continue

46 |
47 |
48 | welcome back 53 |
54 |
55 |
59 |
67 | 68 |
69 |
70 |
71 | 72 |
81 | 82 | 83 |
84 | 89 |
90 | 91 | 94 | setPassword(event.target.value)} 97 | type="password" 98 | id="inputPassword" 99 | className="form-control" 100 | placeholder="Password" 101 | required 102 | /> 103 |
104 | 107 |
108 |
109 | setError(null)} 112 | show={error !== null} 113 | delay={3000} 114 | autohide 115 | > 116 | 117 | 122 | Error 123 | 124 | {error} 125 | 126 |
127 |
128 |
129 | 130 |
131 |
132 | 133 | ); 134 | } 135 | 136 | const UsernameSelect = ({ username, setUsername, names = [""] }) => { 137 | const [open, setOpen] = useState(false); 138 | const [width, setWidth] = useState(0); 139 | const ref = useRef(); 140 | /** @ts-ignore */ 141 | const clientRectWidth = ref.current?.getBoundingClientRect().width; 142 | useEffect(() => { 143 | /** @ts-ignore */ 144 | setWidth(clientRectWidth); 145 | }, [clientRectWidth]); 146 | 147 | /** Click away listener */ 148 | useEffect(() => { 149 | if (open) { 150 | const listener = () => setOpen(false); 151 | document.addEventListener("click", listener); 152 | return () => document.removeEventListener("click", listener); 153 | } 154 | }, [open]); 155 | 156 | /** Make the current div focused */ 157 | useEffect(() => { 158 | if (open) { 159 | /** @ts-ignore */ 160 | ref.current?.focus(); 161 | } 162 | }, [open]); 163 | 164 | return ( 165 |
setOpen((o) => !o)} 170 | > 171 |
172 |
{username}
173 |
174 | 175 | 176 | 177 |
178 |
179 |
183 | {names.map((name) => ( 184 |
setUsername(name)} 188 | > 189 | {name} 190 |
191 | ))} 192 |
193 |
194 | ); 195 | }; 196 | -------------------------------------------------------------------------------- /client/build/static/js/runtime-main.120840cb.js.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["../webpack/bootstrap"],"names":["webpackJsonpCallback","data","moduleId","chunkId","chunkIds","moreModules","executeModules","i","resolves","length","Object","prototype","hasOwnProperty","call","installedChunks","push","modules","parentJsonpFunction","shift","deferredModules","apply","checkDeferredModules","result","deferredModule","fulfilled","j","depId","splice","__webpack_require__","s","installedModules","1","exports","module","l","m","c","d","name","getter","o","defineProperty","enumerable","get","r","Symbol","toStringTag","value","t","mode","__esModule","ns","create","key","bind","n","object","property","p","jsonpArray","this","oldJsonpFunction","slice"],"mappings":"aACE,SAASA,EAAqBC,GAQ7B,IAPA,IAMIC,EAAUC,EANVC,EAAWH,EAAK,GAChBI,EAAcJ,EAAK,GACnBK,EAAiBL,EAAK,GAIHM,EAAI,EAAGC,EAAW,GACpCD,EAAIH,EAASK,OAAQF,IACzBJ,EAAUC,EAASG,GAChBG,OAAOC,UAAUC,eAAeC,KAAKC,EAAiBX,IAAYW,EAAgBX,IACpFK,EAASO,KAAKD,EAAgBX,GAAS,IAExCW,EAAgBX,GAAW,EAE5B,IAAID,KAAYG,EACZK,OAAOC,UAAUC,eAAeC,KAAKR,EAAaH,KACpDc,EAAQd,GAAYG,EAAYH,IAKlC,IAFGe,GAAqBA,EAAoBhB,GAEtCO,EAASC,QACdD,EAASU,OAATV,GAOD,OAHAW,EAAgBJ,KAAKK,MAAMD,EAAiBb,GAAkB,IAGvDe,IAER,SAASA,IAER,IADA,IAAIC,EACIf,EAAI,EAAGA,EAAIY,EAAgBV,OAAQF,IAAK,CAG/C,IAFA,IAAIgB,EAAiBJ,EAAgBZ,GACjCiB,GAAY,EACRC,EAAI,EAAGA,EAAIF,EAAed,OAAQgB,IAAK,CAC9C,IAAIC,EAAQH,EAAeE,GACG,IAA3BX,EAAgBY,KAAcF,GAAY,GAE3CA,IACFL,EAAgBQ,OAAOpB,IAAK,GAC5Be,EAASM,EAAoBA,EAAoBC,EAAIN,EAAe,KAItE,OAAOD,EAIR,IAAIQ,EAAmB,GAKnBhB,EAAkB,CACrBiB,EAAG,GAGAZ,EAAkB,GAGtB,SAASS,EAAoB1B,GAG5B,GAAG4B,EAAiB5B,GACnB,OAAO4B,EAAiB5B,GAAU8B,QAGnC,IAAIC,EAASH,EAAiB5B,GAAY,CACzCK,EAAGL,EACHgC,GAAG,EACHF,QAAS,IAUV,OANAhB,EAAQd,GAAUW,KAAKoB,EAAOD,QAASC,EAAQA,EAAOD,QAASJ,GAG/DK,EAAOC,GAAI,EAGJD,EAAOD,QAKfJ,EAAoBO,EAAInB,EAGxBY,EAAoBQ,EAAIN,EAGxBF,EAAoBS,EAAI,SAASL,EAASM,EAAMC,GAC3CX,EAAoBY,EAAER,EAASM,IAClC5B,OAAO+B,eAAeT,EAASM,EAAM,CAAEI,YAAY,EAAMC,IAAKJ,KAKhEX,EAAoBgB,EAAI,SAASZ,GACX,qBAAXa,QAA0BA,OAAOC,aAC1CpC,OAAO+B,eAAeT,EAASa,OAAOC,YAAa,CAAEC,MAAO,WAE7DrC,OAAO+B,eAAeT,EAAS,aAAc,CAAEe,OAAO,KAQvDnB,EAAoBoB,EAAI,SAASD,EAAOE,GAEvC,GADU,EAAPA,IAAUF,EAAQnB,EAAoBmB,IAC/B,EAAPE,EAAU,OAAOF,EACpB,GAAW,EAAPE,GAA8B,kBAAVF,GAAsBA,GAASA,EAAMG,WAAY,OAAOH,EAChF,IAAII,EAAKzC,OAAO0C,OAAO,MAGvB,GAFAxB,EAAoBgB,EAAEO,GACtBzC,OAAO+B,eAAeU,EAAI,UAAW,CAAET,YAAY,EAAMK,MAAOA,IACtD,EAAPE,GAA4B,iBAATF,EAAmB,IAAI,IAAIM,KAAON,EAAOnB,EAAoBS,EAAEc,EAAIE,EAAK,SAASA,GAAO,OAAON,EAAMM,IAAQC,KAAK,KAAMD,IAC9I,OAAOF,GAIRvB,EAAoB2B,EAAI,SAAStB,GAChC,IAAIM,EAASN,GAAUA,EAAOiB,WAC7B,WAAwB,OAAOjB,EAAgB,SAC/C,WAA8B,OAAOA,GAEtC,OADAL,EAAoBS,EAAEE,EAAQ,IAAKA,GAC5BA,GAIRX,EAAoBY,EAAI,SAASgB,EAAQC,GAAY,OAAO/C,OAAOC,UAAUC,eAAeC,KAAK2C,EAAQC,IAGzG7B,EAAoB8B,EAAI,IAExB,IAAIC,EAAaC,KAAyB,mBAAIA,KAAyB,oBAAK,GACxEC,EAAmBF,EAAW5C,KAAKuC,KAAKK,GAC5CA,EAAW5C,KAAOf,EAClB2D,EAAaA,EAAWG,QACxB,IAAI,IAAIvD,EAAI,EAAGA,EAAIoD,EAAWlD,OAAQF,IAAKP,EAAqB2D,EAAWpD,IAC3E,IAAIU,EAAsB4C,EAI1BxC,I","file":"static/js/runtime-main.120840cb.js","sourcesContent":[" \t// install a JSONP callback for chunk loading\n \tfunction webpackJsonpCallback(data) {\n \t\tvar chunkIds = data[0];\n \t\tvar moreModules = data[1];\n \t\tvar executeModules = data[2];\n\n \t\t// add \"moreModules\" to the modules object,\n \t\t// then flag all \"chunkIds\" as loaded and fire callback\n \t\tvar moduleId, chunkId, i = 0, resolves = [];\n \t\tfor(;i < chunkIds.length; i++) {\n \t\t\tchunkId = chunkIds[i];\n \t\t\tif(Object.prototype.hasOwnProperty.call(installedChunks, chunkId) && installedChunks[chunkId]) {\n \t\t\t\tresolves.push(installedChunks[chunkId][0]);\n \t\t\t}\n \t\t\tinstalledChunks[chunkId] = 0;\n \t\t}\n \t\tfor(moduleId in moreModules) {\n \t\t\tif(Object.prototype.hasOwnProperty.call(moreModules, moduleId)) {\n \t\t\t\tmodules[moduleId] = moreModules[moduleId];\n \t\t\t}\n \t\t}\n \t\tif(parentJsonpFunction) parentJsonpFunction(data);\n\n \t\twhile(resolves.length) {\n \t\t\tresolves.shift()();\n \t\t}\n\n \t\t// add entry modules from loaded chunk to deferred list\n \t\tdeferredModules.push.apply(deferredModules, executeModules || []);\n\n \t\t// run deferred modules when all chunks ready\n \t\treturn checkDeferredModules();\n \t};\n \tfunction checkDeferredModules() {\n \t\tvar result;\n \t\tfor(var i = 0; i < deferredModules.length; i++) {\n \t\t\tvar deferredModule = deferredModules[i];\n \t\t\tvar fulfilled = true;\n \t\t\tfor(var j = 1; j < deferredModule.length; j++) {\n \t\t\t\tvar depId = deferredModule[j];\n \t\t\t\tif(installedChunks[depId] !== 0) fulfilled = false;\n \t\t\t}\n \t\t\tif(fulfilled) {\n \t\t\t\tdeferredModules.splice(i--, 1);\n \t\t\t\tresult = __webpack_require__(__webpack_require__.s = deferredModule[0]);\n \t\t\t}\n \t\t}\n\n \t\treturn result;\n \t}\n\n \t// The module cache\n \tvar installedModules = {};\n\n \t// object to store loaded and loading chunks\n \t// undefined = chunk not loaded, null = chunk preloaded/prefetched\n \t// Promise = chunk loading, 0 = chunk loaded\n \tvar installedChunks = {\n \t\t1: 0\n \t};\n\n \tvar deferredModules = [];\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId]) {\n \t\t\treturn installedModules[moduleId].exports;\n \t\t}\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\ti: moduleId,\n \t\t\tl: false,\n \t\t\texports: {}\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.l = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// define getter function for harmony exports\n \t__webpack_require__.d = function(exports, name, getter) {\n \t\tif(!__webpack_require__.o(exports, name)) {\n \t\t\tObject.defineProperty(exports, name, { enumerable: true, get: getter });\n \t\t}\n \t};\n\n \t// define __esModule on exports\n \t__webpack_require__.r = function(exports) {\n \t\tif(typeof Symbol !== 'undefined' && Symbol.toStringTag) {\n \t\t\tObject.defineProperty(exports, Symbol.toStringTag, { value: 'Module' });\n \t\t}\n \t\tObject.defineProperty(exports, '__esModule', { value: true });\n \t};\n\n \t// create a fake namespace object\n \t// mode & 1: value is a module id, require it\n \t// mode & 2: merge all properties of value into the ns\n \t// mode & 4: return value when already ns object\n \t// mode & 8|1: behave like require\n \t__webpack_require__.t = function(value, mode) {\n \t\tif(mode & 1) value = __webpack_require__(value);\n \t\tif(mode & 8) return value;\n \t\tif((mode & 4) && typeof value === 'object' && value && value.__esModule) return value;\n \t\tvar ns = Object.create(null);\n \t\t__webpack_require__.r(ns);\n \t\tObject.defineProperty(ns, 'default', { enumerable: true, value: value });\n \t\tif(mode & 2 && typeof value != 'string') for(var key in value) __webpack_require__.d(ns, key, function(key) { return value[key]; }.bind(null, key));\n \t\treturn ns;\n \t};\n\n \t// getDefaultExport function for compatibility with non-harmony modules\n \t__webpack_require__.n = function(module) {\n \t\tvar getter = module && module.__esModule ?\n \t\t\tfunction getDefault() { return module['default']; } :\n \t\t\tfunction getModuleExports() { return module; };\n \t\t__webpack_require__.d(getter, 'a', getter);\n \t\treturn getter;\n \t};\n\n \t// Object.prototype.hasOwnProperty.call\n \t__webpack_require__.o = function(object, property) { return Object.prototype.hasOwnProperty.call(object, property); };\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"/\";\n\n \tvar jsonpArray = this[\"webpackJsonpclient\"] = this[\"webpackJsonpclient\"] || [];\n \tvar oldJsonpFunction = jsonpArray.push.bind(jsonpArray);\n \tjsonpArray.push = webpackJsonpCallback;\n \tjsonpArray = jsonpArray.slice();\n \tfor(var i = 0; i < jsonpArray.length; i++) webpackJsonpCallback(jsonpArray[i]);\n \tvar parentJsonpFunction = oldJsonpFunction;\n\n\n \t// run deferred modules from other chunks\n \tcheckDeferredModules();\n"],"sourceRoot":""} -------------------------------------------------------------------------------- /client/build/static/css/main.85225e57.chunk.css: -------------------------------------------------------------------------------- 1 | :root{--primary:#556ee6!important;--light:#f5f5f8!important;--success:#34c38f!important}.bg-success{background-color:#34c38f!important;background-color:var(--success)!important}.bg-light{background-color:#f5f5f8!important;background-color:var(--light)!important}.bg-gray{background-color:var(--gray)!important}.bg-primary{background-color:#556ee6!important;background-color:var(--primary)!important}.text-primary{color:#556ee6!important;color:var(--primary)!important}.list-group-item.active{background-color:#556ee6!important;background-color:var(--primary)!important;border-color:#556ee6!important;border-color:var(--primary)!important}.btn-rounded{border-radius:30px!important}.btn{display:inline-block;font-weight:400;color:#495057;text-align:center;vertical-align:middle;-webkit-user-select:none;-ms-user-select:none;user-select:none;background-color:transparent;border:1px solid transparent;border-radius:30px!important;padding:.47rem .75rem;font-size:.8125rem;line-height:1.5;border-radius:.25rem;transition:color .15s ease-in-out,background-color .15s ease-in-out,border-color .15s ease-in-out,box-shadow .15s ease-in-out}.btn-primary{color:#fff;background-color:#556ee6;background-color:var(--primary);border-color:#556ee6;border-color:var(--primary)}.btn-primary.focus,.btn-primary:focus,.btn-primary:hover{color:#fff;background-color:#3452e1;border-color:#2948df}.font-size-14{font-size:14px!important}.font-size-11{font-size:11px!important}.font-size-12{font-size:12px!important}.font-size-15{font-size:15px!important}.w-md{min-width:110px}body{font-family:"Poppins",Arial,Helvetica,sans-serif;font-size:13px;color:#495057}.navbar{box-shadow:0 12px 24px 0 rgba(18,38,63,.03)}.navbar-brand{font-size:16px}.chats-title{padding-left:14px}.login-page{display:flex;align-items:center;flex-direction:column;justify-content:center;padding-bottom:190px;height:100vh}.form-signin{width:100%;max-width:330px;padding:15px;margin:0 auto}.text-small{font-size:.9rem}.chat-box,.messages-box{width:100%}.chat-box-wrapper{flex:1 1;overflow-y:scroll}.rounded-lg{border-radius:.5rem}input::-webkit-input-placeholder{font-size:.9rem;color:#999}input:-ms-input-placeholder{font-size:.9rem;color:#999}input::-ms-input-placeholder{font-size:.9rem;color:#999}input::placeholder{font-size:.9rem;color:#999}.centered-box{width:100%;height:100vh;display:flex;align-items:center;justify-content:center}.login-error-anchor{position:relative}.toast-box{text-align:left;margin-top:30px;position:absolute;width:100%;top:0;display:flex;flex-direction:row;justify-content:center}.full-height{height:100vh;flex-direction:column;display:flex}.full-height .container{flex:1 1}.container .row{height:100%}.flex-column{display:flex;flex-direction:column}.bg-white.flex-column{height:100%}.flex{flex:1 1}.logout-button{cursor:pointer;display:flex;flex-direction:row;align-items:center;padding:15px 20px}.logout-button svg{margin-right:15px}.no-messages{opacity:.5;height:100%;width:100%}.avatar-box{width:50px;height:50px;object-fit:cover;object-position:50%;overflow:hidden;border-radius:4px}.avatar-box,.user-link{cursor:pointer}.user-link:hover{text-decoration:underline}.online-indicator{width:14px;height:14px;border:2px solid #fff;bottom:-7px;right:-7px;background-color:#4df573}.online-indicator.selected{border:none;width:12px;height:12px;bottom:-5px;right:-5px}.online-indicator.offline{background-color:#bbb}span.pseudo-link{font-size:14px;text-decoration:underline;color:var(--blue);cursor:pointer}span.pseudo-link:hover{text-decoration:none}.list-group-item{cursor:pointer;height:70px;box-sizing:border-box;transition:background-color .1s ease-out}.chat-icon{width:45px;height:45px;border-radius:4px;background-color:#eee}.chat-icon.active{background-color:var(--blue)}.chats-title{font-size:15px}.chat-body{border-radius:10px!important}.chat-list-container{height:100%}.chat-input{border-radius:30px!important;background-color:#eff2f7!important;border-color:#eff2f7!important;padding-right:120px}.form-control::-webkit-input-placeholder{font-size:13px}.form-control:-ms-input-placeholder{font-size:13px}.form-control::-ms-input-placeholder{font-size:13px}.form-control::placeholder{font-size:13px}.form-control{display:block;width:100%;height:calc(1.5em + .94rem + 2px);padding:7.5px 12px;font-size:13px;font-weight:400;line-height:1.5;color:#495057;background-color:#fff;background-clip:padding-box;border:1px solid #ced4da;transition:border-color .15s ease-in-out,box-shadow .15s ease-in-out}.rounded-button{border-radius:30px;background-color:var(--light)}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:300;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:400;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:500;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:600;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0900-097f,U+1cd0-1cf6,U+1cf8-1cf9,U+200c-200d,U+20a8,U+20b9,U+25cc,U+a830-a839,U+a8e0-a8fb}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2) format("woff2");unicode-range:U+0100-024f,U+0259,U+1e??,U+2020,U+20a0-20ab,U+20ad-20cf,U+2113,U+2c60-2c7f,U+a720-a7ff}@font-face{font-family:"Poppins";font-style:normal;font-weight:700;font-display:swap;src:url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2) format("woff2");unicode-range:U+00??,U+0131,U+0152-0153,U+02bb-02bc,U+02c6,U+02da,U+02dc,U+2000-206f,U+2074,U+20ac,U+2122,U+2191,U+2193,U+2212,U+2215,U+feff,U+fffd}.login-form .username-select button{background-color:transparent!important;color:inherit;padding:7.5px 12px!important;display:block!important;border:1px solid #ced4da!important;border-radius:4px!important;width:100%!important;text-align:left!important}.login-form .username-select .btn-primary:not(:disabled):not(.disabled).active,.login-form .username-select .btn-primary:not(:disabled):not(.disabled):active,.show>.btn-primary.dropdown-toggle{color:inherit}.login-form .username-select .dropdown-menu.show.dropdown-menu-right{transform:translateY(38px)!important}.username-select-dropdown{position:relative;display:flex!important;align-items:center;background-color:transparent!important;color:inherit;padding:0 12px!important;border:1px solid #ced4da!important;border-radius:4px!important;width:100%!important;text-align:left!important;height:calc(1.5em + .94rem + 2px)!important;cursor:pointer}.username-select-dropdown .username-select-block{background-color:var(--white);position:absolute;top:-1138px;left:0;opacity:0;transform:scale(.5);transform-origin:top left;transition:opacity .2s ease,transform .2s ease;border:1px solid #ced4da!important;border-radius:4px!important;padding:8px 0}.username-select-dropdown:focus{outline:none;border-color:#80bdff!important;box-shadow:0 0 0 .2rem rgba(0,123,255,.25)}.username-select-dropdown .username-select-block.open{top:42px;transform:scale(1);opacity:1}.username-select-row{display:flex;width:100%;justify-content:space-between;align-items:center}.username-select-dropdown .username-select-block .username-select-block-item{padding:4px 24px}.username-select-dropdown .username-select-block .username-select-block-item:hover{background-color:var(--light)}.chat-list-item{cursor:pointer;padding:14px 16px}.mdi-circle:before{content:"󰝥"}.mdi-set,.mdi:before{display:inline-block;font:normal normal normal 24px/1 Material Design Icons;font-size:inherit;text-rendering:auto;line-height:inherit;-webkit-font-smoothing:antialiased;-moz-osx-font-smoothing:grayscale} 2 | /*# sourceMappingURL=main.85225e57.chunk.css.map */ -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Basic Redis Chat App Demo (Node.js) 2 | 3 | Showcases how to impliment chat app in Node.js, Socket.IO and Redis. This example uses **pub/sub** feature combined with web-sockets for implementing the message communication between client and server. 4 | 5 | 6 | 7 | 8 | # Overview video 9 | 10 | Here's a short video that explains the project and how it uses Redis: 11 | 12 | [![Watch the video on YouTube](https://github.com/redis-developer/basic-redis-chat-app-demo-nodejs/raw/main/README.md)](https://www.youtube.com/watch?v=miK7xDkDXF0) 13 | 14 | ## Technical Stacks 15 | 16 | - Frontend - _React_, _Socket_ (Socket.IO) 17 | - Backend - _Node.js_, _Redis_ 18 | 19 | ## How it works? 20 | 21 | ### Initialization 22 | 23 | For simplicity, a key with **total_users** value is checked: if it does not exist, we fill the Redis database with initial data. 24 | `EXISTS total_users` (checks if the key exists) 25 | 26 | The demo data initialization is handled in multiple steps: 27 | 28 | **Creating of demo users:** 29 | We create a new user id: `INCR total_users`. Then we set a user ID lookup key by user name: **_e.g._** `SET username:nick user:1`. And finally, the rest of the data is written to the hash set: **_e.g._** `HSET user:1 username "nick" password "bcrypt_hashed_password"`. 30 | 31 | Additionally, each user is added to the default "General" room. For handling rooms for each user, we have a set that holds the room ids. Here's an example command of how to add the room: **_e.g._** `SADD user:1:rooms "0"`. 32 | 33 | **Populate private messages between users.** 34 | At first, private rooms are created: if a private room needs to be established, for each user a room id: `room:1:2` is generated, where numbers correspond to the user ids in ascending order. 35 | 36 | **_E.g._** Create a private room between 2 users: `SADD user:1:rooms 1:2` and `SADD user:2:rooms 1:2`. 37 | 38 | Then we add messages to this room by writing to a sorted set: 39 | 40 | **_E.g._** `ZADD room:1:2 1615480369 "{'from': 1, 'date': 1615480369, 'message': 'Hello', 'roomId': '1:2'}"`. 41 | 42 | We use a stringified _JSON_ for keeping the message structure and simplify the implementation details for this demo-app. 43 | 44 | **Populate the "General" room with messages.** Messages are added to the sorted set with id of the "General" room: `room:0` 45 | 46 | ### Registration 47 | 48 | ![How it works](docs/screenshot000.png) 49 | 50 | Redis is used mainly as a database to keep the user/messages data and for sending messages between connected servers. 51 | 52 | #### How the data is stored: 53 | 54 | - The chat data is stored in various keys and various data types. 55 | - User data is stored in a hash set where each user entry contains the next values: 56 | - `username`: unique user name; 57 | - `password`: hashed password 58 | 59 | * User hash set is accessed by key `user:{userId}`. The data for it stored with `HSET key field data`. User id is calculated by incrementing the `total_users`. 60 | 61 | - E.g `INCR total_users` 62 | 63 | * Username is stored as a separate key (`username:{username}`) which returns the userId for quicker access. 64 | - E.g `SET username:Alex 4` 65 | 66 | #### How the data is accessed: 67 | 68 | - **Get User** `HGETALL user:{id}` 69 | 70 | - E.g `HGETALL user:2`, where we get data for the user with id: 2. 71 | 72 | - **Online users:** will return ids of users which are online 73 | - E.g `SMEMBERS online_users` 74 | 75 | #### Code Example: Prepare User Data in Redis HashSet 76 | 77 | ```JavaScript 78 | const usernameKey = makeUsernameKey(username); 79 | /** Create user */ 80 | const hashedPassword = await bcrypt.hash(password, 10); 81 | const nextId = await incr("total_users"); 82 | const userKey = `user:${nextId}`; 83 | await set(usernameKey, userKey); 84 | await hmset(userKey, ["username", username, "password", hashedPassword]); 85 | 86 | /** 87 | * Each user has a set of rooms he is in 88 | * let's define the default ones 89 | */ 90 | await sadd(`user:${nextId}:rooms`, `${0}`); // Main room 91 | ``` 92 | 93 | ### Rooms 94 | 95 | ![How it works](docs/screenshot001.png) 96 | 97 | #### How the data is stored: 98 | 99 | Each user has a set of rooms associated with them. 100 | 101 | **Rooms** are sorted sets which contains messages where score is the timestamp for each message. Each room has a name associated with it. 102 | 103 | - Rooms which user belongs too are stored at `user:{userId}:rooms` as a set of room ids. 104 | 105 | - E.g `SADD user:Alex:rooms 1` 106 | 107 | - Set room name: `SET room:{roomId}:name {name}` 108 | - E.g `SET room:1:name General` 109 | 110 | #### How the data is accessed: 111 | 112 | - **Get room name** `GET room:{roomId}:name`. 113 | 114 | - E. g `GET room:0:name`. This should return "General" 115 | 116 | - **Get room ids of a user:** `SMEMBERS user:{id}:rooms`. 117 | - E. g `SMEMBERS user:2:rooms`. This will return IDs of rooms for user with ID: 2 118 | 119 | #### Code Example: Get all My Rooms 120 | 121 | ```JavaScript 122 | const rooms = []; 123 | for (let x = 0; x < roomIds.length; x++) { 124 | const roomId = roomIds[x]; 125 | 126 | let name = await get(`room:${roomId}:name`); 127 | /** It's a room without a name, likey the one with private messages */ 128 | if (!name) { 129 | /** 130 | * Make sure we don't add private rooms with empty messages 131 | * It's okay to add custom (named rooms) 132 | */ 133 | const roomExists = await exists(`room:${roomId}`); 134 | if (!roomExists) { 135 | continue; 136 | } 137 | 138 | const userIds = roomId.split(":"); 139 | if (userIds.length !== 2) { 140 | return res.sendStatus(400); 141 | } 142 | rooms.push({ 143 | id: roomId, 144 | names: [ 145 | await hmget(`user:${userIds[0]}`, "username"), 146 | await hmget(`user:${userIds[1]}`, "username"), 147 | ], 148 | }); 149 | } else { 150 | rooms.push({ 151 | id: roomId, 152 | names: [name] 153 | }); 154 | } 155 | } 156 | return rooms; 157 | ``` 158 | 159 | ### Messages 160 | 161 | #### Pub/sub 162 | 163 | After initialization, a pub/sub subscription is created: `SUBSCRIBE MESSAGES`. At the same time, each server instance will run a listener on a message on this channel to receive real-time updates. 164 | 165 | Again, for simplicity, each message is serialized to **_JSON_**, which we parse and then handle in the same manner, as WebSocket messages. 166 | 167 | Pub/sub allows connecting multiple servers written in different platforms without taking into consideration the implementation detail of each server. 168 | 169 | #### How the data is stored: 170 | 171 | - Messages are stored at `room:{roomId}` key in a sorted set (as mentioned above). They are added with `ZADD room:{roomId} {timestamp} {message}` command. Message is serialized to an app-specific JSON string. 172 | - E.g `ZADD room:0 1617197047 { "From": "2", "Date": 1617197047, "Message": "Hello", "RoomId": "1:2" }` 173 | 174 | #### How the data is accessed: 175 | 176 | - **Get list of messages** `ZREVRANGE room:{roomId} {offset_start} {offset_end}`. 177 | - E.g `ZREVRANGE room:1:2 0 50` will return 50 messages with 0 offsets for the private room between users with IDs 1 and 2. 178 | 179 | #### Code Example: Send Message 180 | 181 | ```Javascript 182 | async (message) => { 183 | /** Make sure nothing illegal is sent here. */ 184 | message = { 185 | ...message, 186 | message: sanitise(message.message) 187 | }; 188 | /** 189 | * The user might be set as offline if he tried to access the chat from another tab, pinging by message 190 | * resets the user online status 191 | */ 192 | await sadd("online_users", message.from); 193 | /** We've got a new message. Store it in db, then send back to the room. */ 194 | const messageString = JSON.stringify(message); 195 | const roomKey = `room:${message.roomId}`; 196 | /** 197 | * It may be possible that the room is private and new, so it won't be shown on the other 198 | * user's screen, check if the roomKey exist. If not then broadcast message that the room is appeared 199 | */ 200 | const isPrivate = !(await exists(`${roomKey}:name`)); 201 | const roomHasMessages = await exists(roomKey); 202 | if (isPrivate && !roomHasMessages) { 203 | const ids = message.roomId.split(":"); 204 | const msg = { 205 | id: message.roomId, 206 | names: [ 207 | await hmget(`user:${ids[0]}`, "username"), 208 | await hmget(`user:${ids[1]}`, "username"), 209 | ], 210 | }; 211 | publish("show.room", msg); 212 | socket.broadcast.emit(`show.room`, msg); 213 | } 214 | await zadd(roomKey, "" + message.date, messageString); 215 | publish("message", message); 216 | io.to(roomKey).emit("message", message); 217 | } 218 | ``` 219 | 220 | ### Session handling 221 | 222 | The chat server works as a basic _REST_ API which involves keeping the session and handling the user state in the chat rooms (besides the WebSocket/real-time part). 223 | 224 | When a WebSocket/real-time server is instantiated, which listens for the next events: 225 | 226 | **Connection**. A new user is connected. At this point, a user ID is captured and saved to the session (which is cached in Redis). Note, that session caching is language/library-specific and it's used here purely for persistence and maintaining the state between server reloads. 227 | 228 | A global set with `online_users` key is used for keeping the online state for each user. So on a new connection, a user ID is written to that set: 229 | 230 | **E.g.** `SADD online_users 1` (We add user with id 1 to the set **online_users**). 231 | 232 | After that, a message is broadcasted to the clients to notify them that a new user is joined the chat. 233 | 234 | **Disconnect**. It works similarly to the connection event, except we need to remove the user for **online_users** set and notify the clients: `SREM online_users 1` (makes user with id 1 offline). 235 | 236 | **Message**. A user sends a message, and it needs to be broadcasted to the other clients. The pub/sub allows us also to broadcast this message to all server instances which are connected to this Redis: 237 | 238 | `PUBLISH message "{'serverId': 4132, 'type':'message', 'data': {'from': 1, 'date': 1615480369, 'message': 'Hello', 'roomId': '1:2'}}"` 239 | 240 | Note we send additional data related to the type of the message and the server id. Server id is used to discard the messages by the server instance which sends them since it is connected to the same `MESSAGES` channel. 241 | 242 | `type` field of the serialized JSON corresponds to the real-time method we use for real-time communication (connect/disconnect/message). 243 | 244 | `data` is method-specific information. In the example above it's related to the new message. 245 | 246 | #### How the data is stored / accessed: 247 | 248 | The session data is stored in Redis by utilizing the [**connect-redis**](https://www.npmjs.com/package/connect-redis) client. 249 | 250 | ```JavaScript 251 | const session = require("express-session"); 252 | let RedisStore = require("connect-redis")(session); 253 | const sessionMiddleware = session({ 254 | store: new RedisStore({ client: redisClient }), 255 | secret: "keyboard cat", 256 | saveUninitialized: true, 257 | resave: true, 258 | }); 259 | ``` 260 | 261 | ## How to run it locally? 262 | 263 | #### Write in environment variable or Dockerfile actual connection to Redis: 264 | 265 | ``` 266 | REDIS_ENDPOINT_URL = "Redis server URI" 267 | REDIS_PASSWORD = "Password to the server" 268 | ``` 269 | 270 | #### Run frontend 271 | 272 | ```sh 273 | cd client 274 | yarn install 275 | yarn start 276 | ``` 277 | 278 | #### Run backend 279 | 280 | ```sh 281 | yarn install 282 | yarn start 283 | ``` 284 | 285 | ## Try it out 286 | 287 | #### Deploy to Heroku 288 | 289 |

290 | 291 | Deploy to Heorku 292 | 293 |

294 | 295 | #### Deploy to Google Cloud 296 | 297 |

298 | 299 | Run on Google Cloud 300 | 301 |

302 | -------------------------------------------------------------------------------- /server/index.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | const express = require("express"); 3 | const bcrypt = require("bcrypt"); 4 | const session = require("express-session"); 5 | const bodyParser = require("body-parser"); 6 | /** @ts-ignore */ 7 | const randomName = require("node-random-name"); 8 | let RedisStore = require("connect-redis")(session); 9 | const path = require("path"); 10 | const fs = require("fs").promises; 11 | 12 | const { 13 | client: redisClient, 14 | exists, 15 | set, 16 | get, 17 | hgetall, 18 | sadd, 19 | zadd, 20 | hmget, 21 | smembers, 22 | sismember, 23 | srem, 24 | sub, 25 | auth: runRedisAuth, 26 | } = require("./redis"); 27 | const { 28 | createUser, 29 | makeUsernameKey, 30 | createPrivateRoom, 31 | sanitise, 32 | getMessages, 33 | } = require("./utils"); 34 | const { createDemoData } = require("./demo-data"); 35 | const { PORT, SERVER_ID } = require("./config"); 36 | 37 | const app = express(); 38 | const server = require("http").createServer(app); 39 | 40 | /** @type {SocketIO.Server} */ 41 | const io = 42 | /** @ts-ignore */ 43 | require("socket.io")(server); 44 | 45 | const sessionMiddleware = session({ 46 | store: new RedisStore({ client: redisClient }), 47 | secret: "keyboard cat", 48 | saveUninitialized: true, 49 | resave: true, 50 | }); 51 | 52 | const auth = (req, res, next) => { 53 | if (!req.session.user) { 54 | return res.sendStatus(403); 55 | } 56 | next(); 57 | }; 58 | 59 | const publish = (type, data) => { 60 | const outgoing = { 61 | serverId: SERVER_ID, 62 | type, 63 | data, 64 | }; 65 | redisClient.publish("MESSAGES", JSON.stringify(outgoing)); 66 | }; 67 | 68 | const initPubSub = () => { 69 | /** We don't use channels here, since the contained message contains all the necessary data. */ 70 | sub.on("message", (_, message) => { 71 | /** 72 | * @type {{ 73 | * serverId: string; 74 | * type: string; 75 | * data: object; 76 | * }} 77 | **/ 78 | const { serverId, type, data } = JSON.parse(message); 79 | /** We don't handle the pub/sub messages if the server is the same */ 80 | if (serverId === SERVER_ID) { 81 | return; 82 | } 83 | io.emit(type, data); 84 | }); 85 | sub.subscribe("MESSAGES"); 86 | }; 87 | 88 | /** Initialize the app */ 89 | (async () => { 90 | /** Need to submit the password from the local stuff. */ 91 | await runRedisAuth(); 92 | /** We store a counter for the total users and increment it on each register */ 93 | const totalUsersKeyExist = await exists("total_users"); 94 | if (!totalUsersKeyExist) { 95 | /** This counter is used for the id */ 96 | await set("total_users", 0); 97 | /** 98 | * Some rooms have pre-defined names. When the clients attempts to fetch a room, an additional lookup 99 | * is handled to resolve the name. 100 | * Rooms with private messages don't have a name 101 | */ 102 | await set(`room:${0}:name`, "General"); 103 | 104 | /** Create demo data with the default users */ 105 | await createDemoData(); 106 | } 107 | 108 | /** Once the app is initialized, run the server */ 109 | runApp(); 110 | })(); 111 | 112 | async function runApp() { 113 | const repoLinks = await fs 114 | .readFile(path.dirname(__dirname) + "/repo.json") 115 | .then((x) => JSON.parse(x.toString())); 116 | 117 | app.use(bodyParser.json()); 118 | app.use("/", express.static(path.dirname(__dirname) + "/client/build")); 119 | 120 | initPubSub(); 121 | 122 | /** Store session in redis. */ 123 | app.use(sessionMiddleware); 124 | io.use((socket, next) => { 125 | /** @ts-ignore */ 126 | sessionMiddleware(socket.request, socket.request.res || {}, next); 127 | // sessionMiddleware(socket.request, socket.request.res, next); will not work with websocket-only 128 | // connections, as 'socket.request.res' will be undefined in that case 129 | }); 130 | 131 | app.get("/links", (req, res) => { 132 | return res.send(repoLinks); 133 | }); 134 | 135 | io.on("connection", async (socket) => { 136 | if (socket.request.session.user === undefined) { 137 | return; 138 | } 139 | const userId = socket.request.session.user.id; 140 | await sadd("online_users", userId); 141 | 142 | const msg = { 143 | ...socket.request.session.user, 144 | online: true, 145 | }; 146 | 147 | publish("user.connected", msg); 148 | socket.broadcast.emit("user.connected", msg); 149 | 150 | socket.on("room.join", (id) => { 151 | socket.join(`room:${id}`); 152 | }); 153 | 154 | socket.on( 155 | "message", 156 | /** 157 | * @param {{ 158 | * from: string 159 | * date: number 160 | * message: string 161 | * roomId: string 162 | * }} message 163 | **/ 164 | async (message) => { 165 | /** Make sure nothing illegal is sent here. */ 166 | message = { ...message, message: sanitise(message.message) }; 167 | /** 168 | * The user might be set as offline if he tried to access the chat from another tab, pinging by message 169 | * resets the user online status 170 | */ 171 | await sadd("online_users", message.from); 172 | /** We've got a new message. Store it in db, then send back to the room. */ 173 | const messageString = JSON.stringify(message); 174 | const roomKey = `room:${message.roomId}`; 175 | /** 176 | * It may be possible that the room is private and new, so it won't be shown on the other 177 | * user's screen, check if the roomKey exist. If not then broadcast message that the room is appeared 178 | */ 179 | const isPrivate = !(await exists(`${roomKey}:name`)); 180 | const roomHasMessages = await exists(roomKey); 181 | if (isPrivate && !roomHasMessages) { 182 | const ids = message.roomId.split(":"); 183 | const msg = { 184 | id: message.roomId, 185 | names: [ 186 | await hmget(`user:${ids[0]}`, "username"), 187 | await hmget(`user:${ids[1]}`, "username"), 188 | ], 189 | }; 190 | publish("show.room", msg); 191 | socket.broadcast.emit(`show.room`, msg); 192 | } 193 | await zadd(roomKey, "" + message.date, messageString); 194 | publish("message", message); 195 | io.to(roomKey).emit("message", message); 196 | } 197 | ); 198 | socket.on("disconnect", async () => { 199 | const userId = socket.request.session.user.id; 200 | await srem("online_users", userId); 201 | const msg = { 202 | ...socket.request.session.user, 203 | online: false, 204 | }; 205 | publish("user.disconnected", msg); 206 | socket.broadcast.emit("user.disconnected", msg); 207 | }); 208 | }); 209 | 210 | /** Fetch a randomly generated name so users don't have collisions when registering a new user. */ 211 | app.get("/randomname", (_, res) => { 212 | return res.send(randomName({ first: true })); 213 | }); 214 | 215 | /** The request the client sends to check if it has the user is cached. */ 216 | app.get("/me", (req, res) => { 217 | /** @ts-ignore */ 218 | const { user } = req.session; 219 | if (user) { 220 | return res.json(user); 221 | } 222 | /** User not found */ 223 | return res.json(null); 224 | }); 225 | 226 | /** Login/register login */ 227 | app.post("/login", async (req, res) => { 228 | const { username, password } = req.body; 229 | const usernameKey = makeUsernameKey(username); 230 | const userExists = await exists(usernameKey); 231 | if (!userExists) { 232 | const newUser = await createUser(username, password); 233 | /** @ts-ignore */ 234 | req.session.user = newUser; 235 | return res.status(201).json(newUser); 236 | } else { 237 | const userKey = await get(usernameKey); 238 | const data = await hgetall(userKey); 239 | if (await bcrypt.compare(password, data.password)) { 240 | const user = { id: userKey.split(":").pop(), username }; 241 | /** @ts-ignore */ 242 | req.session.user = user; 243 | return res.status(200).json(user); 244 | } 245 | } 246 | // user not found 247 | return res.status(404).json({ message: "Invalid username or password" }); 248 | }); 249 | 250 | app.post("/logout", auth, (req, res) => { 251 | req.session.destroy(() => {}); 252 | return res.sendStatus(200); 253 | }); 254 | 255 | /** 256 | * Create a private room and add users to it 257 | */ 258 | app.post("/room", auth, async (req, res) => { 259 | const { user1, user2 } = { 260 | user1: parseInt(req.body.user1), 261 | user2: parseInt(req.body.user2), 262 | }; 263 | 264 | const [result, hasError] = await createPrivateRoom(user1, user2); 265 | if (hasError) { 266 | return res.sendStatus(400); 267 | } 268 | return res.status(201).send(result); 269 | }); 270 | 271 | /** Fetch messages from the general chat (just to avoid loading them only once the user was logged in.) */ 272 | app.get("/room/0/preload", async (req, res) => { 273 | const roomId = "0"; 274 | try { 275 | let name = await get(`room:${roomId}:name`); 276 | const messages = await getMessages(roomId, 0, 20); 277 | return res.status(200).send({ id: roomId, name, messages }); 278 | } catch (err) { 279 | return res.status(400).send(err); 280 | } 281 | }); 282 | 283 | /** Fetch messages from a selected room */ 284 | app.get("/room/:id/messages", auth, async (req, res) => { 285 | const roomId = req.params.id; 286 | const offset = +req.query.offset; 287 | const size = +req.query.size; 288 | try { 289 | const messages = await getMessages(roomId, offset, size); 290 | return res.status(200).send(messages); 291 | } catch (err) { 292 | return res.status(400).send(err); 293 | } 294 | }); 295 | 296 | /** Check which users are online. */ 297 | app.get(`/users/online`, auth, async (req, res) => { 298 | const onlineIds = await smembers(`online_users`); 299 | const users = {}; 300 | for (let onlineId of onlineIds) { 301 | const user = await hgetall(`user:${onlineId}`); 302 | users[onlineId] = { 303 | id: onlineId, 304 | username: user.username, 305 | online: true, 306 | }; 307 | } 308 | return res.send(users); 309 | }); 310 | 311 | /** Retrieve the user info based on ids sent */ 312 | app.get(`/users`, async (req, res) => { 313 | /** @ts-ignore */ 314 | /** @type {string[]} */ const ids = req.query.ids; 315 | if (typeof ids === "object" && Array.isArray(ids)) { 316 | /** Need to fetch */ 317 | const users = {}; 318 | for (let x = 0; x < ids.length; x++) { 319 | /** @type {string} */ 320 | const id = ids[x]; 321 | const user = await hgetall(`user:${id}`); 322 | users[id] = { 323 | id: id, 324 | username: user.username, 325 | online: !!(await sismember("online_users", id)), 326 | }; 327 | } 328 | return res.send(users); 329 | } 330 | return res.sendStatus(404); 331 | }); 332 | 333 | /** 334 | * Get rooms for the selected user. 335 | * TODO: Add middleware and protect the other user info. 336 | */ 337 | app.get(`/rooms/:userId`, auth, async (req, res) => { 338 | const userId = req.params.userId; 339 | /** We got the room ids */ 340 | const roomIds = await smembers(`user:${userId}:rooms`); 341 | const rooms = []; 342 | for (let x = 0; x < roomIds.length; x++) { 343 | const roomId = roomIds[x]; 344 | 345 | let name = await get(`room:${roomId}:name`); 346 | /** It's a room without a name, likey the one with private messages */ 347 | if (!name) { 348 | /** 349 | * Make sure we don't add private rooms with empty messages 350 | * It's okay to add custom (named rooms) 351 | */ 352 | const roomExists = await exists(`room:${roomId}`); 353 | if (!roomExists) { 354 | continue; 355 | } 356 | 357 | const userIds = roomId.split(":"); 358 | if (userIds.length !== 2) { 359 | return res.sendStatus(400); 360 | } 361 | rooms.push({ 362 | id: roomId, 363 | names: [ 364 | await hmget(`user:${userIds[0]}`, "username"), 365 | await hmget(`user:${userIds[1]}`, "username"), 366 | ], 367 | }); 368 | } else { 369 | rooms.push({ id: roomId, names: [name] }); 370 | } 371 | } 372 | res.status(200).send(rooms); 373 | }); 374 | 375 | /** 376 | * We have an external port from the environment variable. To get this working on heroku, 377 | * it's required to specify the host 378 | */ 379 | if (process.env.PORT) { 380 | server.listen(+PORT, "0.0.0.0", () => 381 | console.log(`Listening on ${PORT}...`) 382 | ); 383 | } else { 384 | server.listen(+PORT, () => console.log(`Listening on ${PORT}...`)); 385 | } 386 | } 387 | -------------------------------------------------------------------------------- /client/build/static/css/main.85225e57.chunk.css.map: -------------------------------------------------------------------------------- 1 | {"version":3,"sources":["webpack://src/styles/style-overrides.css","webpack://src/styles/style.css","webpack://src/styles/font-face.css","webpack://src/components/Login/style.css","webpack://src/components/Chat/components/ChatList/components/ChatListItem/style.css"],"names":[],"mappings":"AAAA,MACE,2BAAuC,CACvC,yBAA2B,CAC3B,2BACF,CAEA,YACE,kCAA2C,CAA3C,yCACF,CAEA,UACE,kCAAyC,CAAzC,uCACF,CAEA,SACE,sCACF,CAEA,YACE,kCAA2C,CAA3C,yCACF,CAEA,cACE,uBAAgC,CAAhC,8BACF,CAEA,wBACE,kCAA2C,CAA3C,yCAA2C,CAC3C,8BAAuC,CAAvC,qCACF,CAEA,aACE,4BACF,CAEA,KACE,oBAAqB,CACrB,eAAgB,CAChB,aAAc,CACd,iBAAkB,CAClB,qBAAsB,CACtB,wBAAiB,CAAjB,oBAAiB,CAAjB,gBAAiB,CACjB,4BAA6B,CAC7B,4BAA6B,CAC7B,4BAA8B,CAC9B,qBAAwB,CACxB,kBAAoB,CACpB,eAAgB,CAChB,oBAAsB,CAGtB,6HAKF,CAEA,aACE,UAAW,CACX,wBAAgC,CAAhC,+BAAgC,CAChC,oBAA4B,CAA5B,2BACF,CAEA,yDAGE,UAAW,CACX,wBAAyB,CACzB,oBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,cACE,wBACF,CAEA,MACE,eACF,CC1FA,KACE,gDAAoD,CACpD,cAAe,CACf,aACF,CAEA,QACE,2CACF,CAEA,cACE,cACF,CAEA,aACE,iBACF,CAEA,YACE,YAAa,CACb,kBAAmB,CACnB,qBAAsB,CACtB,sBAAuB,CACvB,oBAAqB,CACrB,YACF,CAEA,aACE,UAAW,CACX,eAAgB,CAChB,YAAa,CACb,aACF,CAEA,YACE,eACF,CAEA,wBAGE,UACF,CAEA,kBACE,QAAO,CACP,iBACF,CAEA,YACE,mBACF,CAEA,iCACE,eAAiB,CACjB,UACF,CAHA,4BACE,eAAiB,CACjB,UACF,CAHA,6BACE,eAAiB,CACjB,UACF,CAHA,mBACE,eAAiB,CACjB,UACF,CAEA,cACE,UAAW,CACX,YAAa,CACb,YAAa,CACb,kBAAmB,CACnB,sBACF,CAEA,oBACE,iBACF,CAEA,WACE,eAAgB,CAChB,eAAgB,CAChB,iBAAkB,CAClB,UAAW,CACX,KAAM,CACN,YAAa,CACb,kBAAmB,CACnB,sBACF,CAEA,aACE,YAAa,CACb,qBAAsB,CACtB,YACF,CAEA,wBACE,QACF,CAEA,gBACE,WACF,CAEA,aACE,YAAa,CACb,qBACF,CAEA,sBACE,WACF,CAEA,MACE,QACF,CAEA,eACE,cAAe,CACf,YAAa,CACb,kBAAmB,CACnB,kBAAmB,CACnB,iBACF,CAEA,mBACE,iBACF,CAEA,aACE,UAAY,CACZ,WAAY,CACZ,UACF,CAEA,YACE,UAAW,CACX,WAAY,CACZ,gBAAiB,CACjB,mBAAoB,CACpB,eAAgB,CAChB,iBAEF,CAEA,uBAHE,cAKF,CAEA,iBACE,yBACF,CAEA,kBACE,UAAW,CACX,WAAY,CACZ,qBAAuB,CACvB,WAAY,CACZ,UAAW,CACX,wBACF,CAEA,2BACE,WAAY,CACZ,UAAW,CACX,WAAY,CACZ,WAAY,CACZ,UACF,CAEA,0BACE,qBACF,CAEA,iBACE,cAAe,CACf,yBAA0B,CAC1B,iBAAkB,CAClB,cACF,CAEA,uBACE,oBACF,CAEA,iBACE,cAAe,CACf,WAAY,CACZ,qBAAsB,CACtB,wCACF,CAEA,WACE,UAAW,CACX,WAAY,CACZ,iBAAkB,CAClB,qBACF,CAEA,kBACE,4BACF,CAEA,aACE,cACF,CAEA,WACE,4BACF,CAEA,qBACE,WACF,CAEA,YACE,4BAA8B,CAC9B,kCAAoC,CACpC,8BAAgC,CAChC,mBACF,CAEA,yCACE,cACF,CAFA,oCACE,cACF,CAFA,qCACE,cACF,CAFA,2BACE,cACF,CAEA,cACE,aAAc,CACd,UAAW,CACX,iCAAmC,CACnC,kBAAmB,CACnB,cAAe,CACf,eAAgB,CAChB,eAAgB,CAChB,aAAc,CACd,qBAAsB,CACtB,2BAA4B,CAC5B,wBAAyB,CAKzB,oEAGF,CAEA,gBACE,kBAAmB,CACnB,6BACF,CChPA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,qGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,qGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,kGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,0GAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,yGACiB,CACjB,qGAEF,CAEA,WACE,qBAAsB,CACtB,iBAAkB,CAClB,eAAgB,CAChB,iBAAkB,CAClB,sGACiB,CACjB,mJAGF,CCzKA,oCACE,sCAAwC,CACxC,aAAc,CACd,4BAA8B,CAC9B,uBAAyB,CACzB,kCAA+C,CAC/C,2BAA6B,CAC7B,oBAAsB,CACtB,yBACF,CAEA,iMAGE,aACF,CAEA,qEACE,oCACF,CAEA,0BACE,iBAAkB,CAClB,sBAAwB,CACxB,kBAAmB,CACnB,sCAAwC,CACxC,aAAc,CACd,wBAA0B,CAC1B,kCAA+C,CAC/C,2BAA6B,CAC7B,oBAAsB,CACtB,yBAA2B,CAC3B,2CAA8C,CAE9C,cACF,CAEA,iDACE,6BAA8B,CAC9B,iBAAkB,CAClB,WAAY,CACZ,MAAO,CACP,SAAU,CACV,mBAA0B,CAC1B,yBAA0B,CAC1B,8CAAkD,CAElD,kCAA+C,CAC/C,2BAA6B,CAE7B,aACF,CAEA,gCACE,YAAa,CACb,8BAAgC,CAChC,0CACF,CAEA,sDACE,QAAS,CACT,kBAAsB,CACtB,SACF,CAEA,qBACE,YAAa,CACb,UAAW,CACX,6BAA8B,CAC9B,kBACF,CAEA,6EACE,gBACF,CAEA,mFAGE,6BACF,CChFA,gBACE,cAAe,CACf,iBACF,CACA,mBACE,YACF,CAEA,qBAEE,oBAAqB,CACrB,sDAAuD,CACvD,iBAAkB,CAClB,mBAAoB,CACpB,mBAAoB,CACpB,kCAAmC,CACnC,iCACF","file":"main.85225e57.chunk.css","sourcesContent":[":root {\n --primary: rgb(85, 110, 230) !important;\n --light: #f5f5f8 !important;\n --success: rgb(52, 195, 143) !important;\n}\n\n.bg-success {\n background-color: var(--success) !important;\n}\n\n.bg-light {\n background-color: var(--light) !important;\n}\n\n.bg-gray {\n background-color: var(--gray) !important;\n}\n\n.bg-primary {\n background-color: var(--primary) !important;\n}\n\n.text-primary {\n color: var(--primary) !important;\n}\n\n.list-group-item.active {\n background-color: var(--primary) !important;\n border-color: var(--primary) !important;\n}\n\n.btn-rounded {\n border-radius: 30px !important;\n}\n\n.btn {\n display: inline-block;\n font-weight: 400;\n color: #495057;\n text-align: center;\n vertical-align: middle;\n user-select: none;\n background-color: transparent;\n border: 1px solid transparent;\n border-radius: 30px !important;\n padding: 0.47rem 0.75rem;\n font-size: 0.8125rem;\n line-height: 1.5;\n border-radius: 0.25rem;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\n border-color 0.15s ease-in-out, -webkit-box-shadow 0.15s ease-in-out;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\n border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\n transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out,\n border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out,\n -webkit-box-shadow 0.15s ease-in-out;\n}\n\n.btn-primary {\n color: #fff;\n background-color: var(--primary);\n border-color: var(--primary);\n}\n\n.btn-primary.focus,\n.btn-primary:focus,\n.btn-primary:hover {\n color: #fff;\n background-color: #3452e1;\n border-color: #2948df;\n}\n\n.font-size-14 {\n font-size: 14px !important;\n}\n\n.font-size-11 {\n font-size: 11px !important;\n}\n\n.font-size-12 {\n font-size: 12px !important;\n}\n\n.font-size-15 {\n font-size: 15px !important;\n}\n\n.w-md {\n min-width: 110px;\n}\n","body {\r\n font-family: \"Poppins\", Arial, Helvetica, sans-serif;\r\n font-size: 13px;\r\n color: #495057;\r\n}\r\n\r\n.navbar {\r\n box-shadow: rgba(18, 38, 63, 0.03) 0px 12px 24px 0px;\r\n}\r\n\r\n.navbar-brand {\r\n font-size: 16px;\r\n}\r\n\r\n.chats-title {\r\n padding-left: 14px;\r\n}\r\n\r\n.login-page {\r\n display: flex;\r\n align-items: center;\r\n flex-direction: column;\r\n justify-content: center;\r\n padding-bottom: 190px;\r\n height: 100vh;\r\n}\r\n\r\n.form-signin {\r\n width: 100%;\r\n max-width: 330px;\r\n padding: 15px;\r\n margin: 0 auto;\r\n}\r\n\r\n.text-small {\r\n font-size: 0.9rem;\r\n}\r\n\r\n.messages-box,\r\n.chat-box {\r\n /* height: 510px; */\r\n width: 100%;\r\n}\r\n\r\n.chat-box-wrapper {\r\n flex: 1;\r\n overflow-y: scroll;\r\n}\r\n\r\n.rounded-lg {\r\n border-radius: 0.5rem;\r\n}\r\n\r\ninput::placeholder {\r\n font-size: 0.9rem;\r\n color: #999;\r\n}\r\n\r\n.centered-box {\r\n width: 100%;\r\n height: 100vh;\r\n display: flex;\r\n align-items: center;\r\n justify-content: center;\r\n}\r\n\r\n.login-error-anchor {\r\n position: relative;\r\n}\r\n\r\n.toast-box {\r\n text-align: left;\r\n margin-top: 30px;\r\n position: absolute;\r\n width: 100%;\r\n top: 0;\r\n display: flex;\r\n flex-direction: row;\r\n justify-content: center;\r\n}\r\n\r\n.full-height {\r\n height: 100vh;\r\n flex-direction: column;\r\n display: flex;\r\n}\r\n\r\n.full-height .container {\r\n flex: 1;\r\n}\r\n\r\n.container .row {\r\n height: 100%;\r\n}\r\n\r\n.flex-column {\r\n display: flex;\r\n flex-direction: column;\r\n}\r\n\r\n.bg-white.flex-column {\r\n height: 100%;\r\n}\r\n\r\n.flex {\r\n flex: 1;\r\n}\r\n\r\n.logout-button {\r\n cursor: pointer;\r\n display: flex;\r\n flex-direction: row;\r\n align-items: center;\r\n padding: 15px 20px;\r\n}\r\n\r\n.logout-button svg {\r\n margin-right: 15px;\r\n}\r\n\r\n.no-messages {\r\n opacity: 0.5;\r\n height: 100%;\r\n width: 100%;\r\n}\r\n\r\n.avatar-box {\r\n width: 50px;\r\n height: 50px;\r\n object-fit: cover;\r\n object-position: 50%;\r\n overflow: hidden;\r\n border-radius: 4px;\r\n cursor: pointer;\r\n}\r\n\r\n.user-link {\r\n cursor: pointer;\r\n}\r\n\r\n.user-link:hover {\r\n text-decoration: underline;\r\n}\r\n\r\n.online-indicator {\r\n width: 14px;\r\n height: 14px;\r\n border: 2px solid white;\r\n bottom: -7px;\r\n right: -7px;\r\n background-color: #4df573;\r\n}\r\n\r\n.online-indicator.selected {\r\n border: none;\r\n width: 12px;\r\n height: 12px;\r\n bottom: -5px;\r\n right: -5px;\r\n}\r\n\r\n.online-indicator.offline {\r\n background-color: #bbb;\r\n}\r\n\r\nspan.pseudo-link {\r\n font-size: 14px;\r\n text-decoration: underline;\r\n color: var(--blue);\r\n cursor: pointer;\r\n}\r\n\r\nspan.pseudo-link:hover {\r\n text-decoration: none;\r\n}\r\n\r\n.list-group-item {\r\n cursor: pointer;\r\n height: 70px;\r\n box-sizing: border-box;\r\n transition: background-color 0.1s ease-out;\r\n}\r\n\r\n.chat-icon {\r\n width: 45px;\r\n height: 45px;\r\n border-radius: 4px;\r\n background-color: #eee;\r\n}\r\n\r\n.chat-icon.active {\r\n background-color: var(--blue);\r\n}\r\n\r\n.chats-title {\r\n font-size: 15px;\r\n}\r\n\r\n.chat-body {\r\n border-radius: 10px !important;\r\n}\r\n\r\n.chat-list-container {\r\n height: 100%;\r\n}\r\n\r\n.chat-input {\r\n border-radius: 30px !important;\r\n background-color: #eff2f7 !important;\r\n border-color: #eff2f7 !important;\r\n padding-right: 120px;\r\n}\r\n\r\n.form-control::placeholder {\r\n font-size: 13px;\r\n}\r\n\r\n.form-control {\r\n display: block;\r\n width: 100%;\r\n height: calc(1.5em + 0.94rem + 2px);\r\n padding: 7.5px 12px;\r\n font-size: 13px;\r\n font-weight: 400;\r\n line-height: 1.5;\r\n color: #495057;\r\n background-color: #fff;\r\n background-clip: padding-box;\r\n border: 1px solid #ced4da;\r\n -webkit-transition: border-color 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out;\r\n transition: border-color 0.15s ease-in-out, box-shadow 0.15s ease-in-out,\r\n -webkit-box-shadow 0.15s ease-in-out;\r\n}\r\n\r\n.rounded-button {\r\n border-radius: 30px;\r\n background-color: var(--light);\r\n}\r\n","/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 300;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 300;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 300;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLDz8Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJbecnFHGPezSQ.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJnecnFHGPezSQ.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 400;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiEyp8kv8JHgFVrJJfecnFHGPc.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 500;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLGT9Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 600;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLEj6Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n/* devanagari */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z11lFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0900-097F, U+1CD0-1CF6, U+1CF8-1CF9, U+200C-200D, U+20A8,\n U+20B9, U+25CC, U+A830-A839, U+A8E0-A8FB;\n}\n/* latin-ext */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1JlFd2JQEl8qw.woff2)\n format(\"woff2\");\n unicode-range: U+0100-024F, U+0259, U+1E00-1EFF, U+2020, U+20A0-20AB,\n U+20AD-20CF, U+2113, U+2C60-2C7F, U+A720-A7FF;\n}\n/* latin */\n@font-face {\n font-family: \"Poppins\";\n font-style: normal;\n font-weight: 700;\n font-display: swap;\n src: url(https://fonts.gstatic.com/s/poppins/v15/pxiByp8kv8JHgFVrLCz7Z1xlFd2JQEk.woff2)\n format(\"woff2\");\n unicode-range: U+0000-00FF, U+0131, U+0152-0153, U+02BB-02BC, U+02C6, U+02DA,\n U+02DC, U+2000-206F, U+2074, U+20AC, U+2122, U+2191, U+2193, U+2212, U+2215,\n U+FEFF, U+FFFD;\n}\n",".login-form .username-select button {\n background-color: transparent !important;\n color: inherit;\n padding: 7.5px 12px !important;\n display: block !important;\n border: 1px solid rgb(206, 212, 218) !important;\n border-radius: 4px !important;\n width: 100% !important;\n text-align: left !important;\n}\n\n.login-form .username-select .btn-primary:not(:disabled):not(.disabled).active,\n.login-form .username-select .btn-primary:not(:disabled):not(.disabled):active,\n.show > .btn-primary.dropdown-toggle {\n color: inherit;\n}\n\n.login-form .username-select .dropdown-menu.show.dropdown-menu-right {\n transform: translate(0px, 38px) !important;\n}\n\n.username-select-dropdown {\n position: relative;\n display: flex !important;\n align-items: center;\n background-color: transparent !important;\n color: inherit;\n padding: 0 12px !important;\n border: 1px solid rgb(206, 212, 218) !important;\n border-radius: 4px !important;\n width: 100% !important;\n text-align: left !important;\n height: calc(1.5em + 0.94rem + 2px) !important;\n\n cursor: pointer;\n}\n\n.username-select-dropdown .username-select-block {\n background-color: var(--white);\n position: absolute;\n top: -1138px;\n left: 0;\n opacity: 0;\n transform: scale(0.5, 0.5);\n transform-origin: top left;\n transition: opacity 0.2s ease, transform 0.2s ease;\n\n border: 1px solid rgb(206, 212, 218) !important;\n border-radius: 4px !important;\n\n padding: 8px 0px;\n}\n\n.username-select-dropdown:focus {\n outline: none;\n border-color: #80bdff !important;\n box-shadow: 0 0 0 0.2rem rgba(0, 123, 255, 0.25);\n}\n\n.username-select-dropdown .username-select-block.open {\n top: 42px;\n transform: scale(1, 1);\n opacity: 1;\n}\n\n.username-select-row {\n display: flex;\n width: 100%;\n justify-content: space-between;\n align-items: center;\n}\n\n.username-select-dropdown .username-select-block .username-select-block-item {\n padding: 4px 24px;\n}\n\n.username-select-dropdown\n .username-select-block\n .username-select-block-item:hover {\n background-color: var(--light);\n}\n",".chat-list-item {\n cursor: pointer;\n padding: 14px 16px;\n}\n.mdi-circle:before {\n content: \"󰝥\";\n}\n\n.mdi-set,\n.mdi:before {\n display: inline-block;\n font: normal normal normal 24px/1 Material Design Icons;\n font-size: inherit;\n text-rendering: auto;\n line-height: inherit;\n -webkit-font-smoothing: antialiased;\n -moz-osx-font-smoothing: grayscale;\n}\n"]} -------------------------------------------------------------------------------- /client/build/static/js/main.90c51a67.chunk.js: -------------------------------------------------------------------------------- 1 | (this.webpackJsonpclient=this.webpackJsonpclient||[]).push([[0],{129:function(e,t,s){"use strict";s.r(t);var n=s(0),a=(s(68),s(69),s(70),s(71),s(1)),c=s(22),r=s.n(c),o=s(2),i=s(3),l=s(4),d=s.n(l),u=s(8),j=s(135),b=function(e){var t=e.width,s=void 0===t?64:t,a=e.height,c=void 0===a?64:a;return Object(n.jsxs)("svg",{xmlns:"http://www.w3.org/2000/svg",xmlnsXlink:"http://www.w3.org/1999/xlink",viewBox:"0 0 32 32",height:c,width:s,children:[Object(n.jsx)("script",{}),Object(n.jsxs)("defs",{children:[Object(n.jsx)("path",{id:"prefix__a",d:"M45.536 38.764c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26v-3.813s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z"}),Object(n.jsx)("path",{id:"prefix__b",d:"M45.536 28.733c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.935c2.332-.837 3.14-.867 5.126-.14s12.35 4.853 14.312 5.57 2.037 1.31.024 2.36z"})]}),Object(n.jsxs)("g",{transform:"matrix(.84833 0 0 .84833 -7.884 -9.45)",children:[Object(n.jsx)("use",{fill:"#a41e11",xlinkHref:"#prefix__a"}),Object(n.jsx)("path",{d:"M45.536 34.95c-2.013 1.05-12.44 5.337-14.66 6.494s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276-2.04-1.613-.077-2.382l15.332-5.936c2.332-.836 3.14-.867 5.126-.14S43.55 31.87 45.51 32.6s2.037 1.31.024 2.36z",fill:"#d82c20"}),Object(n.jsx)("use",{fill:"#a41e11",xlinkHref:"#prefix__a",y:-6.218}),Object(n.jsx)("use",{fill:"#d82c20",xlinkHref:"#prefix__b"}),Object(n.jsx)("path",{d:"M45.536 26.098c-2.013 1.05-12.44 5.337-14.66 6.495s-3.453 1.146-5.207.308-12.85-5.32-14.85-6.276c-1-.478-1.524-.88-1.524-1.26V21.55s14.447-3.145 16.78-3.982 3.14-.867 5.126-.14 13.853 2.868 15.814 3.587v3.76c0 .377-.452.8-1.477 1.324z",fill:"#a41e11"}),Object(n.jsx)("use",{fill:"#d82c20",xlinkHref:"#prefix__b",y:-6.449}),Object(n.jsxs)("g",{fill:"#fff",children:[Object(n.jsx)("path",{d:"M29.096 20.712l-1.182-1.965-3.774-.34 2.816-1.016-.845-1.56 2.636 1.03 2.486-.814-.672 1.612 2.534.95-3.268.34zM22.8 24.624l8.74-1.342-2.64 3.872z"}),Object(n.jsx)("ellipse",{cx:20.444,cy:21.402,rx:4.672,ry:1.811})]}),Object(n.jsx)("path",{d:"M42.132 21.138l-5.17 2.042-.004-4.087z",fill:"#7a0c00"}),Object(n.jsx)("path",{d:"M36.963 23.18l-.56.22-5.166-2.042 5.723-2.264z",fill:"#ad2115"})]})]})},m=(s(76),["Pablo","Joe","Mary","Alex"]);function f(e){var t=e.onLogIn,s=Object(a.useState)((function(){return m[Math.floor(Math.random()*m.length)]})),c=Object(i.a)(s,2),r=c[0],o=c[1],l=Object(a.useState)("password123"),f=Object(i.a)(l,2),O=f[0],p=f[1],x=Object(a.useState)(null),g=Object(i.a)(x,2),v=g[0],y=g[1],N=function(){var e=Object(u.a)(d.a.mark((function e(s){return d.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:s.preventDefault(),t(r,O,y);case 2:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}();return Object(n.jsx)(n.Fragment,{children:Object(n.jsx)("div",{className:"login-form text-center login-page",children:Object(n.jsxs)("div",{className:"rounded",style:{boxShadow:"0 0.75rem 1.5rem rgba(18,38,63,.03)"},children:[Object(n.jsxs)("div",{className:"position-relative",children:[Object(n.jsxs)("div",{className:"row no-gutters align-items-center",style:{maxWidth:400,backgroundColor:"rgba(85, 110, 230, 0.25)",paddingLeft:20,paddingRight:20,borderTopLeftRadius:4,borderTopRightRadius:4},children:[Object(n.jsxs)("div",{className:"col text-primary text-left",children:[Object(n.jsx)("h3",{className:"font-size-15",children:"Welcome Back !"}),Object(n.jsx)("p",{children:"Sign in to continue"})]}),Object(n.jsx)("div",{className:"col align-self-end",children:Object(n.jsx)("img",{alt:"welcome back",style:{maxWidth:"100%"},src:"".concat("","/welcome-back.png")})})]}),Object(n.jsx)("div",{className:"position-absolute",style:{bottom:-36,left:20},children:Object(n.jsx)("div",{style:{backgroundColor:"rgb(239, 242, 247)",width:72,height:72},className:"rounded-circle d-flex align-items-center justify-content-center",children:Object(n.jsx)(b,{width:34,height:34})})})]}),Object(n.jsxs)("form",{className:"bg-white text-left px-4",style:{paddingTop:58,borderBottomLeftRadius:4,borderBottomRightRadius:4},onSubmit:N,children:[Object(n.jsx)("label",{className:"font-size-12",children:"Name"}),Object(n.jsx)("div",{className:"username-select mb-3",children:Object(n.jsx)(h,{username:r,setUsername:o,names:m})}),Object(n.jsx)("label",{htmlFor:"inputPassword",className:"font-size-12",children:"Password"}),Object(n.jsx)("input",{value:O,onChange:function(e){return p(e.target.value)},type:"password",id:"inputPassword",className:"form-control",placeholder:"Password",required:!0}),Object(n.jsx)("div",{style:{height:30}}),Object(n.jsx)("button",{className:"btn btn-lg btn-primary btn-block",type:"submit",children:"Sign in"}),Object(n.jsx)("div",{className:"login-error-anchor",children:Object(n.jsx)("div",{className:"toast-box",children:Object(n.jsxs)(j.a,{style:{minWidth:277},onClose:function(){return y(null)},show:null!==v,delay:3e3,autohide:!0,children:[Object(n.jsxs)(j.a.Header,{children:[Object(n.jsx)("img",{src:"holder.js/20x20?text=%20",className:"rounded mr-2",alt:""}),Object(n.jsx)("strong",{className:"mr-auto",children:"Error"})]}),Object(n.jsx)(j.a.Body,{children:v})]})})}),Object(n.jsx)("div",{style:{height:30}})]})]})})})}var h=function(e){var t,s=e.username,c=e.setUsername,r=e.names,o=void 0===r?[""]:r,l=Object(a.useState)(!1),d=Object(i.a)(l,2),u=d[0],j=d[1],b=Object(a.useState)(0),m=Object(i.a)(b,2),f=m[0],h=m[1],O=Object(a.useRef)(),p=null===(t=O.current)||void 0===t?void 0:t.getBoundingClientRect().width;return Object(a.useEffect)((function(){h(p)}),[p]),Object(a.useEffect)((function(){if(u){var e=function(){return j(!1)};return document.addEventListener("click",e),function(){return document.removeEventListener("click",e)}}}),[u]),Object(a.useEffect)((function(){var e;u&&(null===(e=O.current)||void 0===e||e.focus())}),[u]),Object(n.jsxs)("div",{tabIndex:0,ref:O,className:"username-select-dropdown ".concat(u?"open":""),onClick:function(){return j((function(e){return!e}))},children:[Object(n.jsxs)("div",{className:"username-select-row",children:[Object(n.jsx)("div",{children:s}),Object(n.jsx)("div",{children:Object(n.jsx)("svg",{width:24,height:24,children:Object(n.jsx)("path",{d:"M7 10l5 5 5-5z",fill:"#333"})})})]}),Object(n.jsx)("div",{style:{width:f},className:"username-select-block ".concat(u?"open":""),children:o.map((function(e){return Object(n.jsx)("div",{className:"username-select-block-item",onClick:function(){return c(e)},children:e},e)}))})]})},O=s(18),p=(s(79),s(12)),x=function(e,t){switch(t.type){case"clear":return{currentRoom:"0",rooms:{},users:{}};case"set user":return Object(o.a)(Object(o.a)({},e),{},{users:Object(o.a)(Object(o.a)({},e.users),{},Object(p.a)({},t.payload.id,t.payload))});case"make user online":return Object(o.a)(Object(o.a)({},e),{},{users:Object(o.a)(Object(o.a)({},e.users),{},Object(p.a)({},t.payload,Object(o.a)(Object(o.a)({},e.users[t.payload]),{},{online:!0})))});case"append users":return Object(o.a)(Object(o.a)({},e),{},{users:Object(o.a)(Object(o.a)({},e.users),t.payload)});case"set messages":return Object(o.a)(Object(o.a)({},e),{},{rooms:Object(o.a)(Object(o.a)({},e.rooms),{},Object(p.a)({},t.payload.id,Object(o.a)(Object(o.a)({},e.rooms[t.payload.id]),{},{messages:t.payload.messages,offset:t.payload.messages.length})))});case"prepend messages":var s=[].concat(Object(O.a)(t.payload.messages),Object(O.a)(e.rooms[t.payload.id].messages));return Object(o.a)(Object(o.a)({},e),{},{rooms:Object(o.a)(Object(o.a)({},e.rooms),{},Object(p.a)({},t.payload.id,Object(o.a)(Object(o.a)({},e.rooms[t.payload.id]),{},{messages:s,offset:s.length})))});case"append message":return void 0===e.rooms[t.payload.id]?e:Object(o.a)(Object(o.a)({},e),{},{rooms:Object(o.a)(Object(o.a)({},e.rooms),{},Object(p.a)({},t.payload.id,Object(o.a)(Object(o.a)({},e.rooms[t.payload.id]),{},{lastMessage:t.payload.message,messages:e.rooms[t.payload.id].messages?[].concat(Object(O.a)(e.rooms[t.payload.id].messages),[t.payload.message]):void 0})))});case"set last message":return Object(o.a)(Object(o.a)({},e),{},{rooms:Object(o.a)(Object(o.a)({},e.rooms),{},Object(p.a)({},t.payload.id,Object(o.a)(Object(o.a)({},e.rooms[t.payload.id]),{},{lastMessage:t.payload.lastMessage})))});case"set current room":return Object(o.a)(Object(o.a)({},e),{},{currentRoom:t.payload});case"add room":return Object(o.a)(Object(o.a)({},e),{},{rooms:Object(o.a)(Object(o.a)({},e.rooms),{},Object(p.a)({},t.payload.id,t.payload))});case"set rooms":var n=t.payload,a=Object(o.a)({},e.rooms);return n.forEach((function(e){a[e.id]=Object(o.a)(Object(o.a)({},e),{},{messages:a[e.id]&&a[e.id].messages})})),Object(o.a)(Object(o.a)({},e),{},{rooms:a});default:return e}},g={currentRoom:"main",rooms:{},users:{}},v=Object(a.createContext)(),y=function(){var e=Object(a.useContext)(v),t=Object(i.a)(e,2);return[t[0],t[1]]},N=function(){return Object(a.useReducer)(x,g)},w=s(17),k=s.n(w),M=s(9),C=s.n(M);C.a.defaults.withCredentials=!0;var L=function(e){return"".concat("").concat(e)},z=function(){return C.a.get(L("/me")).then((function(e){return e.data})).catch((function(e){return null}))},S=function(e,t){return C.a.post(L("/login"),{username:e,password:t}).then((function(e){return e.data})).catch((function(e){throw new Error(e.response&&e.response.data&&e.response.data.message)}))},_=function(){return C.a.post(L("/logout"))},E=function(e){var t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:0,s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:15;return C.a.get(L("/room/".concat(e,"/messages")),{params:{offset:t,size:s}}).then((function(e){return e.data.reverse()}))},R=function(e){return C.a.get(L("/users"),{params:{ids:e}}).then((function(e){return e.data}))},I=function(){var e=Object(u.a)(d.a.mark((function e(t,s){return d.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",C.a.post(L("/room"),{user1:t,user2:s}).then((function(e){return e.data})));case 1:case"end":return e.stop()}}),e)})));return function(t,s){return e.apply(this,arguments)}}(),B=function(){var e=Object(u.a)(d.a.mark((function e(t){return d.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.abrupt("return",C.a.get(L("/rooms/".concat(t))).then((function(e){return e.data})));case 1:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}(),F=s(63),T=function(e,t){var s,n=Object(F.a)(e);try{for(n.s();!(s=n.n()).done;){var a=s.value;if("string"!==typeof a&&(a=a[0]),a!==t)return a}}catch(c){n.e(c)}finally{n.f()}return e[0]},U=(s(130),function(){var e=Object(u.a)(d.a.mark((function e(t,s,n){var a,c,r;return d.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:if(a={},n.forEach((function(e){a[e.from]=1})),0===(c=Object.keys(a).filter((function(e){return void 0===t[e]}))).length){e.next=8;break}return e.next=6,R(c);case 6:r=e.sent,s({type:"append users",payload:r});case 8:case"end":return e.stop()}}),e)})));return function(t,s,n){return e.apply(this,arguments)}}()),H=function(){return Object(n.jsxs)("svg",{width:"32",height:"32",viewBox:"0 0 1651 1651",fill:"none",xmlns:"http://www.w3.org/2000/svg",children:[Object(n.jsx)("rect",{width:"1651",height:"1651",rx:"14",fill:"white"}),Object(n.jsx)("path",{d:"M495.286 1098.96L497.967 1070.86L478.04 1050.88C408.572 981.233 368 891.771 368 795.344C368 585.371 565.306 402 826 402C1086.69 402 1284 585.371 1284 795.344C1284 1005.32 1086.69 1188.69 826 1188.69V1248.69L825.913 1188.69C779.837 1188.75 733.952 1182.77 689.432 1170.9L667.26 1164.98L646.8 1175.37C620.731 1188.61 562.74 1213.98 467.32 1235.35C480.554 1191.83 490.95 1144.39 495.286 1098.96Z",stroke:"url(#paint0_linear)",strokeWidth:"120"}),Object(n.jsx)("defs",{children:Object(n.jsxs)("linearGradient",{id:"paint0_linear",x1:"662.312",y1:"397.956",x2:"416.164",y2:"1678.7",gradientUnits:"userSpaceOnUse",children:[Object(n.jsx)("stop",{stopColor:"#7514FB"}),Object(n.jsx)("stop",{offset:"0.624243",stopColor:"#F26D41"}),Object(n.jsx)("stop",{offset:"1",stopColor:"#F43B4B"})]})})]})},P=function(e){var t=e.name,s=e.id,c=Object(a.useMemo)((function(){var e=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"1",t=13,s=654,n=531,a=+e.split(":").pop(),c=+e.split(":").reverse().pop();c<0&&(c+=3555);var r=(a*s+c*n)%t;return"".concat("","/avatars/").concat(r,".jpg")}(""+s);return"Mary"===t?"".concat("","/avatars/0.jpg"):"Pablo"===t?"".concat("","/avatars/2.jpg"):"Joe"===t?"".concat("","/avatars/9.jpg"):"Alex"===t?"".concat("","/avatars/8.jpg"):e}),[s,t]);return Object(n.jsx)(n.Fragment,{children:"General"!==t?Object(n.jsx)("img",{src:c,alt:t,style:{width:32,height:32,objectFit:"cover"},className:"rounded-circle avatar-xs"}):Object(n.jsx)("div",{className:"overflow-hidden rounded-circle",children:Object(n.jsx)(H,{})})})},W=function(e){var t=e.online,s=e.hide,a=void 0!==s&&s,c=e.width,r=void 0===c?8:c,o=e.height,i=void 0===o?8:o;return Object(n.jsx)("div",{className:t?"rounded-circle bg-success":"rounded-circle bg-gray",style:{width:r,height:i,opacity:a?0:1}})},D=function(e){var t=e.id,s=e.name,n=y(),c=Object(i.a)(n,1)[0],r=Object(a.useMemo)((function(){try{var e=Math.abs(parseInt(t.split(":").reverse().pop())),n=e>0,a=Object.entries(c.users).filter((function(e){return Object(i.a)(e,2)[1].username===s})).map((function(e){return Object(i.a)(e,2)[1]})),r=!1;return a.length>0&&(r=a[0].online,e=+a[0].id),[n,r,e]}catch(o){return[!1,!1,"0"]}}),[t,s,c.users]),o=Object(i.a)(r,3),l=o[0],d=o[1],u=o[2],j=A(e);return{isUser:l,online:d,userId:u,name:e.name,lastMessage:j}},A=function(e){var t=y(),s=Object(i.a)(t,2)[1],n=e.lastMessage;return Object(a.useEffect)((function(){void 0===n&&(void 0===e.messages?E(e.id,0,1).then((function(t){var n=null;0!==t.length&&(n=t.pop()),s({type:"set last message",payload:{id:e.id,lastMessage:n}})})):0===e.messages.length?s({type:"set last message",payload:{id:e.id,lastMessage:null}}):s({type:"set last message",payload:{id:e.id,lastMessage:e.messages[e.messages.length-1]}}))}),[n,s,e]),n},J=function(e){var t=e.room,s=e.active,a=void 0!==s&&s,c=e.onClick,r=D(t),o=r.online,i=r.name,l=r.lastMessage,d=r.userId;return Object(n.jsxs)("div",{onClick:c,className:"chat-list-item d-flex align-items-start rounded ".concat(a?"bg-white":""),children:[Object(n.jsx)("div",{className:"align-self-center mr-3",children:Object(n.jsx)(W,{online:o,hide:"0"===t.id})}),Object(n.jsx)("div",{className:"align-self-center mr-3",children:Object(n.jsx)(P,{name:i,id:d})}),Object(n.jsxs)("div",{className:"media-body overflow-hidden",children:[Object(n.jsx)("h5",{className:"text-truncate font-size-14 mb-1",children:i}),l&&Object(n.jsxs)("p",{className:"text-truncate mb-0",children:[" ",l.message," "]})]}),l&&Object(n.jsx)("div",{className:"font-size-11",children:k.a.unix(l.date).format("LT")})]})},V=s(133),q=function(e){var t=e.onLogOut,s=e.col,a=void 0===s?5:s,c=e.noinfo,r=void 0!==c&&c;return Object(n.jsxs)("div",{onClick:t,style:{cursor:"pointer"},className:"col-".concat(a," text-danger ").concat(r?"":"text-right"),children:[Object(n.jsx)(V.a,{})," Log out"]})},G=function(e){var t=e.user,s=e.col,a=void 0===s?7:s,c=e.noinfo,r=void 0!==c&&c;return Object(n.jsxs)("div",{className:"col-".concat(a," d-flex align-items-center ").concat(r?"justify-content-end":""),children:[Object(n.jsx)("div",{className:"align-self-center ".concat(r?"":"mr-3"),children:Object(n.jsx)(P,{name:t.username,id:t.id})}),!r&&Object(n.jsxs)("div",{className:"media-body",children:[Object(n.jsx)("h5",{className:"font-size-14 mt-0 mb-1",children:t.username}),Object(n.jsxs)("div",{className:"d-flex align-items-center",children:[Object(n.jsx)(W,{online:!0}),Object(n.jsx)("p",{className:"ml-2 text-muted mb-0",children:"Active"})]})]})]})},X=function(e){var t=e.user,s=e.onLogOut;return Object(n.jsx)("div",{className:"row no-gutters align-items-center pl-4 pr-2 pb-3",style:{height:"inherit",flex:0,minHeight:50},children:Object(n.jsxs)(n.Fragment,{children:[Object(n.jsx)(G,{user:t,col:8}),Object(n.jsx)(q,{onLogOut:s,col:4})]})})},Z=function(e){var t=e.rooms,s=e.dispatch,c=e.user,r=e.currentRoom,o=e.onLogOut,i=Object(a.useMemo)((function(){var e=Object.values(t),s=e.filter((function(e){return"0"===e.id})),n=e.filter((function(e){return"0"!==e.id}));return n=n.sort((function(e,t){return+e.id.split(":").pop()-+t.id.split(":").pop()})),[].concat(Object(O.a)(s||[]),Object(O.a)(n))}),[t]);return Object(n.jsx)(n.Fragment,{children:Object(n.jsxs)("div",{className:"chat-list-container flex-column d-flex pr-4",children:[Object(n.jsx)("div",{className:"py-2",children:Object(n.jsx)("p",{className:"h5 mb-0 py-1 chats-title",children:"Chats"})}),Object(n.jsx)("div",{className:"messages-box flex flex-1",children:Object(n.jsx)("div",{className:"list-group rounded-0",children:i.map((function(e){return Object(n.jsx)(J,{onClick:function(){return s({type:"set current room",payload:e.id})},active:r===e.id,room:e},e.id)}))})}),Object(n.jsx)(X,{user:c,onLogOut:o})]})})},K=function(e){var t=e.message;return Object(n.jsx)("p",{className:"mb-2 fs-6 fw-light fst-italic text-black-50 text-center",style:{opacity:.8,fontSize:14},children:t})},Q=function(){return Object(n.jsx)("div",{className:"no-messages flex-column d-flex flex-row justify-content-center align-items-center text-muted text-center",children:Object(n.jsx)("div",{className:"spinner-border",role:"status",children:Object(n.jsx)("span",{className:"visually-hidden"})})})},Y=s(134),$=function(){return Object(n.jsxs)("div",{className:"no-messages flex-column d-flex flex-row justify-content-center align-items-center text-muted text-center",children:[Object(n.jsx)(Y.a,{size:96}),Object(n.jsx)("p",{children:"No messages"})]})},ee=function(){return Object(n.jsxs)("svg",{width:12,height:12,className:"prefix__MuiSvgIcon-root prefix__jss80 prefix__MuiSvgIcon-fontSizeLarge",viewBox:"0 0 24 24","aria-hidden":"true",children:[Object(n.jsx)("path",{d:"M11.99 2C6.47 2 2 6.48 2 12s4.47 10 9.99 10C17.52 22 22 17.52 22 12S17.52 2 11.99 2zM12 20c-4.42 0-8-3.58-8-8s3.58-8 8-8 8 3.58 8 8-3.58 8-8 8z"}),Object(n.jsx)("path",{d:"M12.5 7H11v6l5.25 3.15.75-1.23-4.5-2.67z"})]})},te=function(e){var t=e.username,s=void 0===t?"user":t,a=e.message,c=void 0===a?"Lorem ipsum dolor...":a,r=e.date;return Object(n.jsxs)("div",{className:"d-flex",children:[Object(n.jsx)("div",{style:{flex:1}}),Object(n.jsx)("div",{style:{width:"50%"},className:"text-right mb-4",children:Object(n.jsx)("div",{className:"conversation-list d-inline-block bg-light px-3 py-2",style:{borderRadius:12},children:Object(n.jsxs)("div",{className:"ctext-wrap",children:[Object(n.jsx)("div",{className:"conversation-name text-left text-primary mb-1",style:{fontWeight:600},children:s}),Object(n.jsx)("p",{className:"text-left",children:c}),Object(n.jsxs)("p",{className:"chat-time mb-0",children:[Object(n.jsx)(ee,{})," ",k.a.unix(r).format("LT")," "]})]})})})]})},se=function(e){var t=e.user,s=e.message,a=void 0===s?"Lorem ipsum dolor...":s,c=e.date,r=e.onUserClicked;return Object(n.jsxs)("div",{className:"d-flex",children:[Object(n.jsx)("div",{style:{width:"50%"},className:"text-left mb-4",children:Object(n.jsx)("div",{className:"conversation-list d-inline-block px-3 py-2",style:{borderRadius:12,backgroundColor:"rgba(85, 110, 230, 0.1)"},children:Object(n.jsxs)("div",{className:"ctext-wrap",children:[t&&Object(n.jsxs)("div",{className:"conversation-name text-primary d-flex align-items-center mb-1",children:[Object(n.jsx)("div",{className:"mr-2",style:{fontWeight:600,cursor:"pointer"},onClick:r,children:t.username}),Object(n.jsx)(W,{width:7,height:7,online:t.online})]}),Object(n.jsx)("p",{className:"text-left",children:a}),Object(n.jsxs)("p",{className:"chat-time mb-0",children:[Object(n.jsx)(ee,{})," ",k.a.unix(c).format("LT")," "]})]})})}),Object(n.jsx)("div",{style:{flex:1}})]})},ne=function(e){var t=e.messageListElement,s=e.messages,a=e.room,c=e.onLoadMoreMessages,r=e.user,o=e.onUserClicked,i=e.users;return Object(n.jsxs)("div",{ref:t,className:"chat-box-wrapper position-relative d-flex",children:[void 0===s?Object(n.jsx)(Q,{}):0===s.length?Object(n.jsx)($,{}):Object(n.jsx)(n.Fragment,{}),Object(n.jsx)("div",{className:"px-4 pt-5 chat-box position-absolute",children:s&&0!==s.length&&Object(n.jsxs)(n.Fragment,{children:[a.offset&&a.offset>=15?Object(n.jsxs)("div",{className:"d-flex flex-row align-items-center mb-4",children:[Object(n.jsx)("div",{style:{height:1,backgroundColor:"#eee",flex:1}}),Object(n.jsx)("div",{className:"mx-3",children:Object(n.jsx)("button",{"aria-haspopup":"true","aria-expanded":"true",type:"button",onClick:c,className:"btn rounded-button btn-secondary nav-btn",id:"__BVID__168__BV_toggle_",children:"Load more"})}),Object(n.jsx)("div",{style:{height:1,backgroundColor:"#eee",flex:1}})]}):Object(n.jsx)(n.Fragment,{}),s.map((function(e,t){var s=e.message+e.date+e.from+t;return"info"===e.from?Object(n.jsx)(K,{message:e.message},s):+e.from!==+r.id?Object(n.jsx)(se,{onUserClicked:function(){return o(e.from)},message:e.message,date:e.date,user:i[e.from]},s):Object(n.jsx)(te,{username:i[e.from]?i[e.from].username:"",message:e.message,date:e.date},s)}))]})})]})},ae=function(e){var t=e.message,s=e.setMessage,a=e.onSubmit;return Object(n.jsx)("div",{className:"p-3 chat-input-section",children:Object(n.jsxs)("form",{className:"row",onSubmit:a,children:[Object(n.jsx)("div",{className:"col",children:Object(n.jsx)("div",{className:"position-relative",children:Object(n.jsx)("input",{value:t,onChange:function(e){return s(e.target.value)},type:"text",placeholder:"Enter Message...",className:"form-control chat-input"})})}),Object(n.jsx)("div",{className:"col-auto",children:Object(n.jsxs)("button",{type:"submit",className:"btn btn-primary btn-rounded chat-send w-md",children:[Object(n.jsx)("span",{className:"d-none d-sm-inline-block mr-2",children:"Send"}),Object(n.jsx)("svg",{width:13,height:13,viewBox:"0 0 24 24",tabIndex:-1,children:Object(n.jsx)("path",{d:"M2.01 21L23 12 2.01 3 2 10l15 2-15 2z",fill:"white"})})]})})]})})},ce=function(e){var t=y(),s=Object(i.a)(t,2),n=s[0],c=s[1],r=Object(a.useRef)(null),l=n.rooms[n.currentRoom],j=null===l||void 0===l?void 0:l.id,b=null===l||void 0===l?void 0:l.messages,m=Object(a.useState)(""),f=Object(i.a)(m,2),h=f[0],O=f[1],p=Object(a.useCallback)((function(){setTimeout((function(){r.current&&(r.current.scrollTop=0)}),0)}),[]),x=Object(a.useCallback)((function(){r.current&&r.current.scrollTo({top:r.current.scrollHeight})}),[]);Object(a.useEffect)((function(){x()}),[b,x]);var g=Object(a.useCallback)((function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:0,t=arguments.length>1&&void 0!==arguments[1]&&arguments[1];E(j,e).then(function(){var e=Object(u.a)(d.a.mark((function e(s){return d.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:return e.next=2,U(n.users,c,s);case 2:c({type:t?"prepend messages":"set messages",payload:{id:j,messages:s}}),t?setTimeout((function(){p()}),10):x();case 4:case"end":return e.stop()}}),e)})));return function(t){return e.apply(this,arguments)}}())}),[c,j,x,p,n.users]);Object(a.useEffect)((function(){void 0!==j&&void 0===b&&g()}),[b,c,j,n.users,n,x,g]),Object(a.useEffect)((function(){r.current&&x()}),[x,j]);var v=function(){var t=Object(u.a)(d.a.mark((function t(s){var a,r,i;return d.a.wrap((function(t){for(;;)switch(t.prev=t.next){case 0:if(a=n.users[s],void 0!==(r=a.room)){t.next=9;break}return t.next=5,I(s,e.id);case 5:i=t.sent,r=i.id,c({type:"set user",payload:Object(o.a)(Object(o.a)({},a),{},{room:r})}),c({type:"add room",payload:{id:r,name:T(i.names,e.username)}});case 9:c({type:"set current room",payload:r});case 10:case"end":return t.stop()}}),t)})));return function(e){return t.apply(this,arguments)}}();return{onLoadMoreMessages:Object(a.useCallback)((function(){g(l.offset,!0)}),[g,l]),onUserClicked:v,message:h,setMessage:O,dispatch:c,room:l,rooms:n.rooms,currentRoom:n.currentRoom,messageListElement:r,roomId:j,users:n.users,messages:b}};function re(e){var t=e.onLogOut,s=e.user,a=e.onMessageSend,c=ce(s),r=c.onLoadMoreMessages,o=c.onUserClicked,i=c.message,l=c.setMessage,d=c.rooms,u=c.room,j=c.currentRoom,b=c.dispatch,m=c.messageListElement,f=c.roomId,h=c.messages,O=c.users;return Object(n.jsx)("div",{className:"container py-5 px-4",children:Object(n.jsxs)("div",{className:"chat-body row overflow-hidden shadow bg-light rounded",children:[Object(n.jsx)("div",{className:"col-4 px-0",children:Object(n.jsx)(Z,{user:s,onLogOut:t,rooms:d,currentRoom:j,dispatch:b})}),Object(n.jsxs)("div",{className:"col-8 px-0 flex-column bg-white rounded-lg",children:[Object(n.jsx)("div",{className:"px-4 py-4",style:{borderBottom:"1px solid #eee"},children:Object(n.jsx)("h2",{className:"font-size-15 mb-0",children:u?u.name:"Room"})}),Object(n.jsx)(ne,{messageListElement:m,messages:h,room:u,onLoadMoreMessages:r,user:s,onUserClicked:o,users:O}),Object(n.jsx)(ae,{message:i,setMessage:l,onSubmit:function(e){e.preventDefault(),a(i.trim(),f),l(""),m.current.scrollTop=m.current.scrollHeight}})]})]})})}function oe(){return Object(n.jsx)("div",{className:"centered-box",children:Object(n.jsx)("div",{className:"spinner-border",role:"status",children:Object(n.jsx)("span",{className:"visually-hidden"})})})}var ie=function(e){var t=e.link;return Object(n.jsx)("a",{href:t,target:"_blank",title:"Github",children:Object(n.jsxs)("svg",{width:24,height:24,viewBox:"0 0 64 64","aria-labelledby":"title","aria-describedby":"desc",role:"img",children:[Object(n.jsx)("path",{"data-name":"layer2",d:"M32 0a32.021 32.021 0 0 0-10.1 62.4c1.6.3 2.2-.7 2.2-1.5v-6c-8.9 1.9-10.8-3.8-10.8-3.8-1.5-3.7-3.6-4.7-3.6-4.7-2.9-2 .2-1.9.2-1.9 3.2.2 4.9 3.3 4.9 3.3 2.9 4.9 7.5 3.5 9.3 2.7a6.93 6.93 0 0 1 2-4.3c-7.1-.8-14.6-3.6-14.6-15.8a12.27 12.27 0 0 1 3.3-8.6 11.965 11.965 0 0 1 .3-8.5s2.7-.9 8.8 3.3a30.873 30.873 0 0 1 8-1.1 30.292 30.292 0 0 1 8 1.1c6.1-4.1 8.8-3.3 8.8-3.3a11.965 11.965 0 0 1 .3 8.5 12.1 12.1 0 0 1 3.3 8.6c0 12.3-7.5 15-14.6 15.8a7.746 7.746 0 0 1 2.2 5.9v8.8c0 .9.6 1.8 2.2 1.5A32.021 32.021 0 0 0 32 0z",fill:"#595F70"}),Object(n.jsx)("path",{"data-name":"layer1",d:"M12.1 45.9c-.1.2-.3.2-.5.1s-.4-.3-.3-.5.3-.2.6-.1c.2.2.3.4.2.5zm1.3 1.5a.589.589 0 0 1-.8-.8.631.631 0 0 1 .7.1.494.494 0 0 1 .1.7zm1.3 1.8a.585.585 0 0 1-.7-.3.6.6 0 0 1 0-.8.585.585 0 0 1 .7.3c.2.3.2.7 0 .8zm1.7 1.8c-.2.2-.5.1-.8-.1-.3-.3-.4-.6-.2-.8a.619.619 0 0 1 .8.1.554.554 0 0 1 .2.8zm2.4 1c-.1.3-.4.4-.8.3s-.6-.4-.5-.7.4-.4.8-.3c.3.2.6.5.5.7zm2.6.2c0 .3-.3.5-.7.5s-.7-.2-.7-.5.3-.5.7-.5c.4.1.7.3.7.5zm2.4-.4q0 .45-.6.6a.691.691 0 0 1-.8-.3q0-.45.6-.6c.5-.1.8.1.8.3z",fill:"#595F70"})]})})},le=function(){var e=Object(a.useState)(null),t=Object(i.a)(e,2),s=t[0],c=t[1];return Object(a.useEffect)((function(){C.a.get(L("/links")).then((function(e){return e.data})).catch((function(e){return null})).then(c)}),[]),Object(n.jsxs)("nav",{className:"navbar navbar-expand-lg navbar-light bg-white",children:[Object(n.jsx)("span",{className:"navbar-brand",children:"Redis chat demo"}),null!==s?Object(n.jsx)("span",{className:"navbar-text",children:s.github&&Object(n.jsx)(ie,{link:s.github})}):Object(n.jsx)(n.Fragment,{})]})},de=s(64),ue=s.n(de),je=function(e,t,s){t({type:"set user",payload:e}),void 0!==s&&t({type:"append message",payload:{id:"0",message:{date:1e4*Math.random(),from:"info",message:s}}})},be=function(){var e=N(),t=Object(i.a)(e,2),s=t[0],n=t[1],c=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:function(e){},t=arguments.length>1?arguments[1]:void 0,s=Object(a.useState)(!0),n=Object(i.a)(s,2),c=n[0],r=n[1],o=Object(a.useState)(null),l=Object(i.a)(o,2),j=l[0],b=l[1],m=function(){var e=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"",t=arguments.length>1&&void 0!==arguments[1]?arguments[1]:"",s=arguments.length>2&&void 0!==arguments[2]?arguments[2]:function(){},n=arguments.length>3&&void 0!==arguments[3]?arguments[3]:function(){};s(null),n(!0),S(e,t).then((function(e){b(e)})).catch((function(e){return s(e.message)})).finally((function(){return n(!1)}))},f=function(){var e=Object(u.a)(d.a.mark((function e(){return d.a.wrap((function(e){for(;;)switch(e.prev=e.next){case 0:_().then((function(){b(null),t({type:"clear"}),r(!0)}));case 1:case"end":return e.stop()}}),e)})));return function(){return e.apply(this,arguments)}}();return Object(a.useEffect)((function(){c&&z().then((function(t){b(t),r(!1),e(t)}))}),[e,c]),{user:j,onLogIn:m,onLogOut:f,loading:c}}(Object(a.useCallback)((function(e){null!==e&&(s.users[e.id]||n({type:"set user",payload:Object(o.a)(Object(o.a)({},e),{},{online:!0})}))}),[n,s.users]),n),r=c.user,l=c.onLogIn,j=c.onLogOut,b=c.loading,m=function(e,t){var s=Object(a.useState)(!1),n=Object(i.a)(s,2),c=n[0],r=n[1],o=Object(a.useRef)(null),l=o.current;return Object(a.useEffect)((function(){null===e?(null!==l&&l.disconnect(),r(!1)):(null!==l?l.connect():o.current=ue()(),r(!0))}),[e,l]),Object(a.useEffect)((function(){c&&e?(l.on("user.connected",(function(e){je(e,t,"".concat(e.username," connected"))})),l.on("user.disconnected",(function(e){return je(e,t,"".concat(e.username," left"))})),l.on("show.room",(function(s){console.log({user:e}),t({type:"add room",payload:{id:s.id,name:T(s.names,e.username)}})})),l.on("message",(function(e){t({type:"make user online",payload:e.from}),t({type:"append message",payload:{id:void 0===e.roomId?"0":e.roomId,message:e}})}))):l&&(l.off("user.connected"),l.off("user.disconnected"),l.off("user.room"),l.off("message"))}),[c,e,t,l]),[l,c]}(r,n),f=Object(i.a)(m,2),h=f[0],O=f[1];Object(a.useEffect)((function(){if(null!==r)if(O){var e=[];Object.keys(s.rooms).forEach((function(t){var n=s.rooms[t];n.connected||(e.push(Object(o.a)(Object(o.a)({},n),{},{connected:!0})),h.emit("room.join",n.id))})),0!==e.length&&n({type:"set rooms",payload:e})}else{var t=[];Object.keys(s.rooms).forEach((function(e){var n=s.rooms[e];n.connected&&t.push(Object(o.a)(Object(o.a)({},n),{},{connected:!1}))})),0!==t.length&&n({type:"set rooms",payload:t})}}),[r,O,n,h,s.rooms,s.users]),Object(a.useEffect)((function(){0===Object.values(s.rooms).length&&null!==r&&(C.a.get(L("/users/online")).then((function(e){return e.data})).then((function(e){n({type:"append users",payload:e})})),B(r.id).then((function(e){var t=[];e.forEach((function(e){var s=e.id,n=e.names;t.push({id:s,name:T(n,r.username)})})),n({type:"set rooms",payload:t}),n({type:"set current room",payload:"0"})})))}),[n,s.rooms,r]);var p=Object(a.useCallback)((function(e,t){"string"===typeof e&&0!==e.trim().length&&(h||console.error("Couldn't send message"),h.emit("message",{roomId:t,message:e,from:r.id,date:k()(new Date).unix()}))}),[r,h]);return{loading:b,user:r,state:s,dispatch:n,onLogIn:l,onMessageSend:p,onLogOut:j}},me=function(){var e=be(),t=e.loading,s=e.user,a=e.state,c=e.dispatch,r=e.onLogIn,o=e.onMessageSend,i=e.onLogOut;if(t)return Object(n.jsx)(oe,{});var l=!s;return Object(n.jsx)(v.Provider,{value:[a,c],children:Object(n.jsxs)("div",{className:"full-height ".concat(l?"bg-light":""),style:{backgroundColor:l?void 0:"#495057"},children:[Object(n.jsx)(le,{}),l?Object(n.jsx)(f,{onLogIn:r}):Object(n.jsx)(re,{user:s,onMessageSend:o,onLogOut:i})]})})};r.a.render(Object(n.jsx)(me,{}),document.getElementById("root"))},69:function(e,t,s){},70:function(e,t,s){},71:function(e,t,s){},76:function(e,t,s){},79:function(e,t,s){}},[[129,1,2]]]); 2 | //# sourceMappingURL=main.90c51a67.chunk.js.map --------------------------------------------------------------------------------