├── 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 |
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 |
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 |
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 |
9 |
10 |
11 |
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 | You need to enable JavaScript to run this app.
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 |
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 |
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 |
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 |
35 | );
36 |
37 | const UserInfo = ({ user, col = 7, noinfo = false }) => (
38 |
43 |
46 | {!noinfo && (
47 |
48 |
{user.username}
49 |
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 You need to enable JavaScript to run this app.
--------------------------------------------------------------------------------
/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 |
20 | Redis chat demo
21 | {links !== null ? (
22 |
23 | {links.github && }
24 |
25 | ) : (
26 | <>>
27 | )}
28 |
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 |
47 | Load more
48 |
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 |
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 |
53 |
54 |
55 |
70 |
71 |
72 |
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 |
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 | [](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 | 
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 | 
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 |
292 |
293 |
294 |
295 | #### Deploy to Google Cloud
296 |
297 |
298 |
299 |
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
--------------------------------------------------------------------------------