First Commit

This commit is contained in:
yuvraj0028
2024-01-04 20:11:35 +05:30
commit 7a876d2d84
13 changed files with 3054 additions and 0 deletions

1
.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
/node_modules

2219
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

22
package.json Normal file
View File

@@ -0,0 +1,22 @@
{
"name": "7-chat-app",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "",
"license": "ISC",
"dependencies": {
"bad-words": "^3.0.4",
"express": "^4.18.2",
"moment": "^2.29.4",
"socket.io": "^4.7.2"
},
"devDependencies": {
"nodemon": "^3.0.2"
}
}

68
public/chat.html Normal file
View File

@@ -0,0 +1,68 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat App</title>
<link rel="icon" href="./img/favicon.png">
<link rel="stylesheet" href="./css/styles.css">
</head>
<body>
<div class="chat">
<div id="sidebar" class="chat__sidebar">
</div>
<div class="chat__main">
<div id="messages" class="chat__messages"></div>
<div class="compose">
<!-- <button id="increment">+1</button> -->
<form id="message-form">
<input type="text" name="message" placeholder="Enter message" autocomplete="off">
<button type="submit">Send</button>
</form>
<button id="send-location">Send Location</button>
</div>
</div>
</div>
<script id="message-template" type="text/html">
<div class="message">
<p>
<span class="message__name">{{username}}</span>
<span class="message__meta">{{createdAt}}</span>
</p>
<p>{{message}}</p>
</div>
</script>
<script id="locmessage-template" type="text/html">
<div class="message">
<p>
<span class="message__name">{{username}}</span>
<span class="message__meta">{{createdAt}}</span>
</p>
<p><a href="{{url}}" target="_blank" style="color: #7C5CBF;">My Current Location</a></p>
</div>
</script>
<script id="sidebar-template" type="text/html">
<h2 class="room-title" >Room: {{room}}</h2>
<h3 class="list-title" >Users</h3>
<ul class="users" >
{{#users}}
<li> {{username}} </li>
{{/users}}
</ul>
</script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/mustache.js/3.0.1/mustache.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/moment.js/2.22.2/moment.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/qs/6.6.0/qs.min.js"></script>
<script src="/socket.io/socket.io.js"></script>
<script src="./js/chat.js"></script>
</body>
</html>

BIN
public/css/.DS_Store vendored Normal file

Binary file not shown.

183
public/css/styles.css Normal file
View File

@@ -0,0 +1,183 @@
/* General Styles */
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
html {
font-size: 16px;
}
input {
font-size: 14px;
}
body {
line-height: 1.4;
color: #333333;
font-family: Helvetica, Arial, sans-serif;
}
h1 {
margin-bottom: 16px;
}
label {
display: block;
font-size: 14px;
margin-bottom: 8px;
color: #777;
}
input {
border: 1px solid #eeeeee;
padding: 12px;
outline: none;
}
button {
cursor: pointer;
padding: 12px;
background: #7C5CBF;
border: none;
color: white;
font-size: 16px;
transition: background .3s ease;
}
button:hover {
background: #6b47b8;
}
button:disabled {
cursor: default;
background: #7c5cbf94;
}
/* Join Page Styles */
.centered-form {
background: #333744;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center;
}
.centered-form__box {
box-shadow: 0px 0px 17px 1px #1D1F26;
background: #F7F7FA;
padding: 24px;
width: 250px;
}
.centered-form button {
width: 100%;
}
.centered-form input {
margin-bottom: 16px;
width: 100%;
}
/* Chat Page Layout */
.chat {
display: flex;
}
.chat__sidebar {
height: 100vh;
color: white;
background: #333744;
width: 225px;
overflow-y: scroll
}
/* Chat styles */
.chat__main {
flex-grow: 1;
display: flex;
flex-direction: column;
max-height: 100vh;
}
.chat__messages {
flex-grow: 1;
padding: 24px 24px 0 24px;
overflow-y: scroll;
}
/* Message Styles */
.message {
margin-bottom: 16px;
}
.message__name {
font-weight: 600;
font-size: 14px;
margin-right: 8px;
}
.message__meta {
color: #777;
font-size: 14px;
}
.message a {
color: #0070CC;
}
/* Message Composition Styles */
.compose {
display: flex;
flex-shrink: 0;
margin-top: 16px;
padding: 24px;
}
.compose form {
display: flex;
flex-grow: 1;
margin-right: 16px;
}
.compose input {
border: 1px solid #eeeeee;
width: 100%;
padding: 12px;
margin: 0 16px 0 0;
flex-grow: 1;
}
.compose button {
font-size: 14px;
}
/* Chat Sidebar Styles */
.room-title {
font-weight: 400;
font-size: 22px;
background: #2c2f3a;
padding: 24px;
}
.list-title {
font-weight: 500;
font-size: 18px;
margin-bottom: 4px;
padding: 12px 24px 0 24px;
}
.users {
list-style-type: none;
font-weight: 300;
padding: 12px 24px 0 24px;
}

169
public/css/styles.min.css vendored Normal file
View File

@@ -0,0 +1,169 @@
* {
margin: 0;
padding: 0;
box-sizing: border-box
}
html {
font-size: 16px
}
input {
font-size: 14px
}
body {
line-height: 1.4;
color: #333;
font-family: Helvetica, Arial, sans-serif
}
h1 {
margin-bottom: 16px
}
label {
display: block;
font-size: 14px;
margin-bottom: 8px;
color: #777
}
input {
border: 1px solid #eee;
padding: 12px;
outline: none
}
button {
cursor: pointer;
padding: 12px;
background: #7C5CBF;
border: none;
color: #fff;
font-size: 16px;
transition: background .3s ease
}
button:hover {
background: #6b47b8
}
button:disabled {
cursor: default;
background: #7c5cbf94
}
.centered-form {
background: #333744;
width: 100vw;
height: 100vh;
display: flex;
justify-content: center;
align-items: center
}
.centered-form__box {
box-shadow: 0 0 17px 1px #1D1F26;
background: #F7F7FA;
padding: 24px;
width: 250px
}
.centered-form button {
width: 100%
}
.centered-form input {
margin-bottom: 16px;
width: 100%
}
.chat {
display: flex
}
.chat__sidebar {
height: 100vh;
color: #fff;
background: #333744;
width: 225px;
overflow-y: scroll
}
.chat__main {
flex-grow: 1;
display: flex;
flex-direction: column;
max-height: 100vh
}
.chat__messages {
flex-grow: 1;
padding: 24px 24px 0;
overflow-y: scroll
}
.message {
margin-bottom: 16px
}
.message__name {
font-weight: 600;
font-size: 14px;
margin-right: 8px
}
.message__meta {
color: #777;
font-size: 14px
}
.message a {
color: #0070CC
}
.compose {
display: flex;
flex-shrink: 0;
margin-top: 16px;
padding: 24px
}
.compose form {
display: flex;
flex-grow: 1;
margin-right: 16px
}
.compose input {
border: 1px solid #eee;
width: 100%;
padding: 12px;
margin: 0 16px 0 0;
flex-grow: 1
}
.compose button {
font-size: 14px
}
.room-title {
font-weight: 400;
font-size: 22px;
background: #2c2f3a;
padding: 24px
}
.list-title {
font-weight: 500;
font-size: 18px;
margin-bottom: 4px;
padding: 12px 24px 0
}
.users {
list-style-type: none;
font-weight: 300;
padding: 12px 24px 0
}

BIN
public/img/favicon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

28
public/index.html Normal file
View File

@@ -0,0 +1,28 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Chat App</title>
<link rel="icon" href="./img/favicon.png">
<link rel="stylesheet" href="./css/styles.css">
</head>
<body>
<div class="centered-form">
<div class="centered-form__box">
<h1>Join</h1>
<form action="./chat.html">
<label>Display Name</label>
<input type="text" name="username" required>
<label>Room</label>
<input type="text" name="room" required>
<button>Join</button>
</form>
</div>
</div>
</body>
</html>

149
public/js/chat.js Normal file
View File

@@ -0,0 +1,149 @@
const socket = io();
// Elements
const $messageForm = document.querySelector("#message-form");
const $messageFormInput = $messageForm.querySelector("input");
const $messageFormButton = $messageForm.querySelector("button");
const $sendLocationButton = document.querySelector("#send-location");
const $messages = document.querySelector("#messages");
// Templates
const messageTemplate = document.querySelector("#message-template").innerHTML;
const locationTemplate = document.querySelector(
"#locmessage-template"
).innerHTML;
const sidebarTemplate = document.querySelector("#sidebar-template").innerHTML;
// Options
const { username, room } = Qs.parse(location.search, {
ignoreQueryPrefix: true,
});
const autoScroll = () => {
// New message element
const $newMessage = $messages.lastElementChild;
// hight of the new message
const newMessageStyle = getComputedStyle($newMessage);
const newMessageMargin = parseInt(newMessageStyle.marginBottom);
const newMessageHeight = $newMessage.offsetHeight + newMessageMargin;
// visible height
const visibleHeight = $messages.offsetHeight;
// height of messages container
const containerHeight = $messages.scrollHeight;
// how far have I scrolled?
const scrollOffset = $messages.scrollTop + visibleHeight;
if (containerHeight - newMessageHeight <= scrollOffset) {
$messages.scrollTop = $messages.scrollHeight;
}
};
// server (emit) -> client (receive) - countUpdated
// client (emit) -> server (receive) - increment
socket.on("message", (message) => {
// console.log(message);
const html = Mustache.render(messageTemplate, {
username: message.username,
message: message.text,
createdAt: moment(message.createdAt).format("h:mm a"),
});
$messages.insertAdjacentHTML("beforeend", html);
autoScroll();
});
socket.on("locationMessage", (url) => {
// console.log(url.username);
const html = Mustache.render(locationTemplate, {
username: url.username,
url: url.url,
createdAt: moment(url.createdAt).format("h:mm a"),
});
$messages.insertAdjacentHTML("beforeend", html);
autoScroll();
});
socket.on("roomData", ({ room, users }) => {
const html = Mustache.render(sidebarTemplate, {
room: room,
users: users,
});
document.querySelector("#sidebar").innerHTML = html;
});
$messageForm.addEventListener("submit", (e) => {
e.preventDefault();
// disable form after submit
$messageFormButton.setAttribute("disabled", "disabled");
//disable
const message = $messageFormInput.value;
if (message === "") {
// enable form after submit
$messageFormButton.removeAttribute("disabled");
return;
}
socket.emit("sendMessage", message, (error) => {
// enable form after submit
$messageFormButton.removeAttribute("disabled");
// clear input
$messageFormInput.value = "";
// focus input
$messageFormInput.focus();
if (error) {
return console.log(error);
}
// console.log("Message Delivered");
});
});
document.querySelector("#send-location").addEventListener("click", (e) => {
e.preventDefault();
if (!navigator.geolocation) {
return alert("Geolocation is not supported by your browser");
}
navigator.permissions.query({ name: "geolocation" }).then((res) => {
// console.log(res);
if (res.state === "denied") {
return alert("Please allow permission to send location!");
}
});
navigator.geolocation.getCurrentPosition((position) => {
// console.log(position);
$sendLocationButton.setAttribute("disabled", "disabled");
socket.emit(
"sendLocation",
{
Latitude: position.coords.latitude,
Longitude: position.coords.longitude,
},
() => {
$sendLocationButton.removeAttribute("disabled");
// console.log("Location Shared");
}
);
});
});
socket.emit("join", { username, room }, (error) => {
if (error) {
alert(error);
location.href = "/";
}
});
// document.querySelector("#increment").addEventListener("click", (e) => {
// console.log("clicked");
// socket.emit("increment");
// });

134
src/index.js Normal file
View File

@@ -0,0 +1,134 @@
// emit events
// socket.emit, io.emit, socket.broadcast.emit
// emit to a specific room
// io.to(room).emit, socket.broadcast.to(room).emit
const express = require("express");
const path = require("path");
const http = require("http");
const socketio = require("socket.io");
const Filter = require("bad-words");
const {
generateMessage,
generateLocationMessage,
} = require("./utils/messages");
const {
addUser,
removeUser,
getUser,
getUsersInRoom,
} = require("./utils/user");
// initialize express
const app = express();
// initialize http server
const server = http.createServer(app);
// initialize socketio
const io = socketio(server);
const port = process.env.PORT || 3000;
// define paths for express config
const publicDirectoryPath = path.join(__dirname, "../public");
// setup static directory to serve
app.use(express.static(publicDirectoryPath));
// let count = 0;
// server (emit) -> client (receive) - countUpdated
// client (emit) -> server (receive) - increment
// let's listen for new connections
io.on("connection", (socket) => {
console.log("New WebSocket connection");
// socket.emit("message", generateMessage("Welcome!"));
// socket.broadcast.emit("message", "A new user has joined!");
socket.on("join", ({ username, room }, callback) => {
// specifically emit event according to room name eg: no one can check whats going on in another room
const { error, user } = addUser({ id: socket.id, username, room });
if (error) {
return callback(error);
}
socket.join(room);
socket.emit("message", generateMessage("Admin", "Welcome!"));
socket.broadcast
.to(user.room)
.emit("message", generateMessage("Admin", `${user.username} has joined`));
io.to(user.room).emit("roomData", {
room: user.room,
users: getUsersInRoom(user.room),
});
callback();
});
socket.on("sendMessage", (message, callback) => {
const filter = new Filter();
const user = getUser(socket.id);
if (!user) {
return callback("You are not authenticated");
}
// if (filter.isProfane(message)) {
// return callback("Profanity is not allowed!");
// }
io.to(user.room).emit("message", generateMessage(user.username, message));
callback();
});
// socket.emit("countUpdated", count);
// socket.on("increment", () => {
// count++;
// notify only the current connection
// socket.emit("countUpdated", count);
// notify all connections
// io.emit("countUpdated", count);
// });
socket.on("disconnect", () => {
const user = removeUser(socket.id);
if (user) {
io.to(user.room).emit(
"message",
generateMessage("Admin", `${user.username} has left`)
);
io.to(user.room).emit("roomData", {
room: user.room,
users: getUsersInRoom(user.room),
});
}
});
socket.on("sendLocation", (coords, callback) => {
const user = getUser(socket.id);
if (!user) {
return callback("You are not authenticated");
}
io.to(user.room).emit(
"locationMessage",
generateLocationMessage(
user.username,
`https://google.com/maps?q=${coords.Latitude},${coords.Longitude}`
)
);
callback();
});
});
// start the server
server.listen(port, () => {
console.log(`Server is up on port ${port}!`);
});

24
src/utils/messages.js Normal file
View File

@@ -0,0 +1,24 @@
// Date time :: getDate, getTime, setDate and so on...
// getTime gives a positive number starting from 1 which depicts the time after 1970
// if we try to access before 1970 in JS than it would gives a negative number
const generateMessage = (username, text) => {
return {
username: username,
text: text,
createdAt: new Date().getTime(),
};
};
const generateLocationMessage = (username, url) => {
return {
username: username,
url: url,
createdAt: new Date().getTime(),
};
};
module.exports = {
generateMessage,
generateLocationMessage,
};

57
src/utils/user.js Normal file
View File

@@ -0,0 +1,57 @@
const users = [];
// addUser, removeUser, getUser, getUserInRoom
const addUser = ({ id, username, room }) => {
// Clean the data
username = username.trim().toLowerCase();
room = room.trim().toLowerCase();
// validate the data
if (!username || !room) {
return {
error: "Username and room are required!",
};
}
// Check for existing user
const existingUser = users.find((user) => {
return user.room === room && user.username === username;
});
// validate username
if (existingUser) {
return {
error: "Username is in use!",
};
}
//Store user
const user = { id, username, room };
users.push(user);
return { user };
};
const removeUser = (id) => {
const index = users.findIndex((user) => user.id === id);
if (index !== -1) {
return users.splice(index, 1)[0];
}
};
const getUser = (id) => {
return users.find((user) => user.id === id);
};
const getUsersInRoom = (room) => {
room = room.trim().toLowerCase();
return users.filter((user) => user.room === room);
};
module.exports = {
addUser,
removeUser,
getUser,
getUsersInRoom,
};