├── README.md ├── chat.js ├── public ├── app.css ├── chat.html ├── client.js └── index.html └── server.js /README.md: -------------------------------------------------------------------------------- 1 | # Real-time Chat App with Deno and Websockets 2 | 3 | ## Demo 4 | 5 | #### Check [the following link](https://deno-websocket-chat.herokuapp.com/chat.html) to see deployed version on heroku 6 | 7 | -------------------- 8 | 9 | ## Installation 10 | You need to have [Deno installed](https://deno.land/#installation) in order to run this app locally 11 | 12 | 1. Clone the repository 13 | 2. Go to the project root using terminal 14 | 3. Run `deno run --allow-net --allow-read server.js` 15 | 4. Open http://localhost:3000/chat.html` in browser 16 | 5. That's all. 17 | 18 | 19 | > The project was created along with Youtube Video ["Build Realtime Chat App with Deno and WebSockets"](https://youtu.be/XWyUtYL6ynE). 20 | > I appreaciate if you like the video and share it. 21 | -------------------------------------------------------------------------------- /chat.js: -------------------------------------------------------------------------------- 1 | // @ts-nocheck 2 | import { isWebSocketCloseEvent } from "https://deno.land/std@0.58.0/ws/mod.ts"; 3 | import { v4 } from "https://deno.land/std@0.58.0/uuid/mod.ts"; 4 | 5 | /** 6 | * userId: { 7 | * userId: string, 8 | * name: string, 9 | * groupName: string, 10 | * ws: WebSocket 11 | * } 12 | */ 13 | const usersMap = new Map(); 14 | 15 | /** 16 | * groupName: [user1, user2] 17 | * 18 | * { 19 | * userId: string, 20 | * name: string, 21 | * groupName: string, 22 | * ws: WebSocket 23 | * } 24 | */ 25 | const groupsMap = new Map(); 26 | 27 | /** 28 | * groupName: [message1,message2] 29 | * 30 | * { 31 | * userId: string, 32 | * name: string, 33 | * message: string 34 | * } 35 | * 36 | */ 37 | const messagesMap = new Map(); 38 | 39 | // This is called when user is connected 40 | export default async function chat(ws) { 41 | // Generate unique userId 42 | const userId = v4.generate(); 43 | 44 | // Listening of WebSocket events 45 | for await (let data of ws) { 46 | const event = typeof data === "string" ? JSON.parse(data) : data; 47 | 48 | // If event is close, 49 | if (isWebSocketCloseEvent(data)) { 50 | // Take out user from usersMap 51 | leaveGroup(userId); 52 | break; 53 | } 54 | 55 | let userObj; 56 | // Check received data.event 57 | switch (event.event) { 58 | // If it is join 59 | case "join": 60 | // Create userObj with ws, groupName and name 61 | userObj = { 62 | userId, 63 | name: event.name, 64 | groupName: event.groupName, 65 | ws, 66 | }; 67 | 68 | // Put userObj inside usersMap 69 | usersMap.set(userId, userObj); 70 | 71 | // Take out users from groupsMap 72 | const users = groupsMap.get(event.groupName) || []; 73 | users.push(userObj); 74 | groupsMap.set(event.groupName, users); 75 | 76 | // Emit to all users in this group that new user joined. 77 | emitUserList(event.groupName); 78 | // Emit all previous messages sent in this group to newly joined user 79 | emitPreviousMessages(event.groupName, ws); 80 | break; 81 | // If it is message receive 82 | case "message": 83 | userObj = usersMap.get(userId); 84 | const message = { 85 | userId, 86 | name: userObj.name, 87 | message: event.data, 88 | }; 89 | const messages = messagesMap.get(userObj.groupName) || []; 90 | messages.push(message); 91 | messagesMap.set(userObj.groupName, messages); 92 | emitMessage(userObj.groupName, message, userId); 93 | break; 94 | } 95 | } 96 | } 97 | 98 | function emitUserList(groupName) { 99 | // Get users from groupsMap 100 | const users = groupsMap.get(groupName) || []; 101 | // Iterate over users and send list of users to every user in the group 102 | for (const user of users) { 103 | const event = { 104 | event: "users", 105 | data: getDisplayUsers(groupName), 106 | }; 107 | user.ws.send(JSON.stringify(event)); 108 | } 109 | } 110 | 111 | function getDisplayUsers(groupName) { 112 | const users = groupsMap.get(groupName) || []; 113 | return users.map((u) => { 114 | return { userId: u.userId, name: u.name }; 115 | }); 116 | } 117 | 118 | function emitMessage(groupName, message, senderId) { 119 | const users = groupsMap.get(groupName) || []; 120 | for (const user of users) { 121 | const tmpMessage = { 122 | ...message, 123 | sender: user.userId === senderId ? "me" : senderId, 124 | }; 125 | const event = { 126 | event: "message", 127 | data: tmpMessage, 128 | }; 129 | user.ws.send(JSON.stringify(event)); 130 | } 131 | } 132 | 133 | function emitPreviousMessages(groupName, ws) { 134 | const messages = messagesMap.get(groupName) || []; 135 | 136 | const event = { 137 | event: "previousMessages", 138 | data: messages, 139 | }; 140 | ws.send(JSON.stringify(event)); 141 | } 142 | 143 | function leaveGroup(userId) { 144 | // Take out users from groupsMap 145 | const userObj = usersMap.get(userId); 146 | if (!userObj) { 147 | return; 148 | } 149 | let users = groupsMap.get(userObj.groupName) || []; 150 | 151 | // Remove current user from users and write users back into groupsMap 152 | users = users.filter((u) => u.userId !== userId); 153 | groupsMap.set(userObj.groupName, users); 154 | 155 | // Remove userId from usersMap 156 | usersMap.delete(userId); 157 | 158 | emitUserList(userObj.groupName); 159 | } 160 | -------------------------------------------------------------------------------- /public/app.css: -------------------------------------------------------------------------------- 1 | * { 2 | box-sizing: border-box; 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | :root { 8 | --main-color: #3b474d; 9 | --main-color-light: #89ebeb; 10 | --main-color-dark: #263238; 11 | } 12 | 13 | html, body { 14 | height: 100%; 15 | } 16 | 17 | body { 18 | font-family: Roboto, sans-serif; 19 | } 20 | 21 | .input-group { 22 | margin-bottom: 20px; 23 | } 24 | 25 | input[type='text'] { 26 | outline: 0; 27 | display: block; 28 | width: 100%; 29 | padding: 6px 10px; 30 | border: 2px solid rgb(209, 209, 209); 31 | transition: all 0.3s; 32 | } 33 | 34 | input[type='text']:focus { 35 | border: 2px solid rgb(184, 184, 184); 36 | } 37 | 38 | button { 39 | outline: 0; 40 | padding: 6px 10px; 41 | border: 1px solid var(--main-color); 42 | background-color: #FFF; 43 | } 44 | 45 | button:active { 46 | box-shadow: inset 1px 2px 3px rgba(0, 0, 0, 0.3); 47 | position: relative; 48 | top: 1px; 49 | } 50 | 51 | .page-login { 52 | display: flex; 53 | align-items: center; 54 | justify-content: center; 55 | } 56 | 57 | .login-form { 58 | width: 400px; 59 | color: white; 60 | margin: 0 auto; 61 | background-color: var(--main-color); 62 | box-shadow: 0px 0px 4px rgba(0, 0, 0, 0.2); 63 | } 64 | 65 | .login-form .form-header, .login-form .form-footer { 66 | background-color: var(--main-color-dark); 67 | } 68 | 69 | .login-form .form-header, .login-form .form-content, .login-form .form-footer { 70 | padding: 15px; 71 | } 72 | 73 | .login-form .form-footer { 74 | text-align: right; 75 | } 76 | 77 | .login-form h2 { 78 | text-align: center; 79 | } 80 | 81 | .login-form .category-item { 82 | margin: 10px 0; 83 | display: block; 84 | width: 100%; 85 | padding-left: 20px; 86 | } 87 | 88 | /* Chat page */ 89 | .chat-container { 90 | display: flex; 91 | flex-wrap: wrap; 92 | height: 100%; 93 | color: #FFF; 94 | background-color: var(--main-color); 95 | overflow: hidden; 96 | } 97 | 98 | .chat-header, .chat-sidebar, .chat-footer { 99 | padding: 15px; 100 | } 101 | 102 | .chat-header { 103 | display: flex; 104 | align-items: center; 105 | width: 100%; 106 | height: 60px; 107 | } 108 | 109 | .chat-header .group-name { 110 | flex: 1; 111 | } 112 | 113 | .chat-header .online-users { 114 | width: 200px; 115 | } 116 | 117 | .chat-main { 118 | display: flex; 119 | flex-direction: column; 120 | flex: 1; 121 | height: 100%; 122 | overflow: hidden; 123 | } 124 | 125 | .chat-sidebar { 126 | width: 240px; 127 | height: 100%; 128 | background-color: var(--main-color-dark); 129 | } 130 | 131 | .chat-sidebar h3 { 132 | margin-bottom: 20px; 133 | } 134 | 135 | .chat-sidebar .chat-user { 136 | padding: 10px 4px; 137 | } 138 | 139 | .chat-messages { 140 | flex: 1; 141 | background-color: #FFF; 142 | color: var(--main-color-dark); 143 | padding: 15px; 144 | display: flex; 145 | flex-direction: column; 146 | overflow: auto; 147 | } 148 | 149 | .chat-messages .message { 150 | border-radius: 8px; 151 | align-self: flex-start; 152 | padding: 6px 10px; 153 | background-color: #dedede; 154 | margin-bottom: 10px; 155 | } 156 | 157 | .chat-messages .message .message-text { 158 | font-size: 80%; 159 | } 160 | 161 | .chat-messages .message.message-to { 162 | background-color: var(--main-color); 163 | color: #FFF; 164 | align-self: flex-end; 165 | } 166 | 167 | .chat-footer form { 168 | display: flex; 169 | } -------------------------------------------------------------------------------- /public/chat.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |
5 | 6 | 7 |