├── frontend
├── public
│ └── bg.jpg
├── postcss.config.js
├── src
│ ├── assets
│ │ └── sounds
│ │ │ └── notification.mp3
│ ├── zustand
│ │ └── useConversation.js
│ ├── utils
│ │ ├── extractTime.js
│ │ └── emoji.js
│ ├── components
│ │ ├── sidebar
│ │ │ ├── Sidebar.jsx
│ │ │ ├── LogoutButton.jsx
│ │ │ ├── Conversations.jsx
│ │ │ ├── Conversation.jsx
│ │ │ └── SearchInput.jsx
│ │ ├── skeletons
│ │ │ └── MessageSkeletons.jsx
│ │ └── messages
│ │ │ ├── Messages.jsx
│ │ │ ├── MessageInput.jsx
│ │ │ ├── Message.jsx
│ │ │ └── MessageContainer.jsx
│ ├── pages
│ │ ├── home
│ │ │ └── Home.jsx
│ │ ├── signup
│ │ │ ├── GenderCheckbox.jsx
│ │ │ └── SignUp.jsx
│ │ └── login
│ │ │ └── Login.jsx
│ ├── context
│ │ ├── AuthContext.jsx
│ │ └── SocketContext.jsx
│ ├── main.jsx
│ ├── hooks
│ │ ├── useListenMessages.js
│ │ ├── useGetConversations.js
│ │ ├── useLogout.js
│ │ ├── useGetMessages.js
│ │ ├── useSendMessage.js
│ │ ├── useLogin.js
│ │ └── useSignup.js
│ ├── App.jsx
│ └── index.css
├── tailwind.config.js
├── index.html
├── vite.config.js
├── README.md
├── .eslintrc.cjs
└── package.json
├── backend
├── routes
│ ├── user.routes.js
│ ├── auth.routes.js
│ └── message.routes.js
├── db
│ ├── connectToMongoDB.js
│ └── connectToRedis.js
├── models
│ ├── message.model.js
│ ├── conversation.model.js
│ └── user.model.js
├── utils
│ └── generateToken.js
├── middleware
│ └── protectRoute.js
├── socket
│ └── socket.js
├── controller
│ ├── user.controller.js
│ ├── message.controller.js
│ └── auth.controller.js
└── server.js
├── .gitignore
├── package.json
├── LICENSE
└── README.md
/frontend/public/bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kriti-raj/messenger-web-app/HEAD/frontend/public/bg.jpg
--------------------------------------------------------------------------------
/frontend/postcss.config.js:
--------------------------------------------------------------------------------
1 | export default {
2 | plugins: {
3 | tailwindcss: {},
4 | autoprefixer: {},
5 | },
6 | }
7 |
--------------------------------------------------------------------------------
/frontend/src/assets/sounds/notification.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/kriti-raj/messenger-web-app/HEAD/frontend/src/assets/sounds/notification.mp3
--------------------------------------------------------------------------------
/frontend/tailwind.config.js:
--------------------------------------------------------------------------------
1 | /** @type {import('tailwindcss').Config} */
2 | export default {
3 | content: [
4 | "./index.html",
5 | "./src/**/*.{js,ts,jsx,tsx}",
6 | ],
7 | theme: {
8 | extend: {},
9 | },
10 | // eslint-disable-next-line no-undef
11 | plugins: [require("daisyui")],
12 | }
--------------------------------------------------------------------------------
/backend/routes/user.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import protectRoute from "../middleware/protectRoute.js";
3 | import { getUsersForSidebar } from "../controller/user.controller.js";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/", protectRoute, getUsersForSidebar)
8 |
9 | export default router;
--------------------------------------------------------------------------------
/backend/routes/auth.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { signup, login, logout } from "../controller/auth.controller.js"
3 | const router = express.Router();
4 |
5 | router.post("/signup", signup);
6 |
7 | router.post("/login", login);
8 |
9 | router.post("/logout", logout);
10 |
11 | export default router;
--------------------------------------------------------------------------------
/frontend/src/zustand/useConversation.js:
--------------------------------------------------------------------------------
1 | import { create } from 'zustand'
2 |
3 | const useConversation = create(set => ({
4 | selectedConversation: null,
5 | setSelectedConversation: (selectedConversation) => set({ selectedConversation }),
6 | messages: [],
7 | setMessages: (messages) => set({ messages }),
8 | }))
9 |
10 | export default useConversation;
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 | logs
3 | *.log
4 | npm-debug.log*
5 | yarn-debug.log*
6 | yarn-error.log*
7 | pnpm-debug.log*
8 | lerna-debug.log*
9 |
10 | node_modules
11 | dist
12 | dist-ssr
13 | *.local
14 |
15 | # Editor directories and files
16 | .vscode/*
17 | !.vscode/extensions.json
18 | .idea
19 | .DS_Store
20 | *.suo
21 | *.ntvs*
22 | *.njsproj
23 | *.sln
24 | *.sw?
25 |
26 |
27 | .env
--------------------------------------------------------------------------------
/backend/routes/message.routes.js:
--------------------------------------------------------------------------------
1 | import express from "express";
2 | import { getMessages, sendMessage } from "../controller/message.controller.js"
3 | import protectRoute from "../middleware/protectRoute.js";
4 |
5 | const router = express.Router();
6 |
7 | router.get("/:id", protectRoute, getMessages)
8 | router.post("/send/:id", protectRoute, sendMessage)
9 |
10 | export default router;
--------------------------------------------------------------------------------
/backend/db/connectToMongoDB.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const connectToMongoDB = async () => {
4 | try {
5 | await mongoose.connect(process.env.MONGO_DB_URI);
6 | console.log("Connected to MongoDB");
7 | } catch (error) {
8 | console.log("Error connecting to MongoDB", error.message);
9 | }
10 | }
11 |
12 | export default connectToMongoDB;
13 |
--------------------------------------------------------------------------------
/frontend/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Messanger App
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
--------------------------------------------------------------------------------
/frontend/src/utils/extractTime.js:
--------------------------------------------------------------------------------
1 | export function extractTime(dataString) {
2 | const data = new Date(dataString);
3 | const hours = padZero(data.getHours());
4 | const minutes = padZero(data.getMinutes());
5 | return `${hours}:${minutes}`;
6 | }
7 |
8 | // Helper function to pad single-digit numbers with a leading zero
9 | function padZero(number) {
10 | return number.toString().padStart(2, '0');
11 | }
--------------------------------------------------------------------------------
/frontend/vite.config.js:
--------------------------------------------------------------------------------
1 | import { defineConfig } from 'vite';
2 | import react from '@vitejs/plugin-react';
3 |
4 | // https://vitejs.dev/config/
5 | export default defineConfig({
6 | plugins: [react()],
7 | server: {
8 | port: 5000,
9 | proxy: {
10 | '/api': {
11 | target: 'http://localhost:3000',
12 | changeOrigin: true,
13 | secure: false,
14 | },
15 | },
16 | },
17 | });
18 |
--------------------------------------------------------------------------------
/frontend/README.md:
--------------------------------------------------------------------------------
1 | # React + Vite
2 |
3 | This template provides a minimal setup to get React working in Vite with HMR and some ESLint rules.
4 |
5 | Currently, two official plugins are available:
6 |
7 | - [@vitejs/plugin-react](https://github.com/vitejs/vite-plugin-react/blob/main/packages/plugin-react/README.md) uses [Babel](https://babeljs.io/) for Fast Refresh
8 | - [@vitejs/plugin-react-swc](https://github.com/vitejs/vite-plugin-react-swc) uses [SWC](https://swc.rs/) for Fast Refresh
9 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/Sidebar.jsx:
--------------------------------------------------------------------------------
1 | import SearchInput from "./SearchInput";
2 | import Conversations from "./Conversations";
3 | import LogoutButton from "./LogoutButton";
4 |
5 | const Sidebar = () => {
6 | return (
7 |
13 | );
14 | };
15 |
16 | export default Sidebar;
17 |
--------------------------------------------------------------------------------
/frontend/src/pages/home/Home.jsx:
--------------------------------------------------------------------------------
1 | import Sidebar from "../../components/sidebar/Sidebar";
2 | import MessageContainer from "../../components/messages/MessageContainer";
3 |
4 | const Home = () => {
5 | return (
6 |
7 |
8 |
9 |
10 | );
11 | };
12 |
13 | export default Home;
14 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/LogoutButton.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import { BiLogOut } from "react-icons/bi";
3 | import useLogout from "../../hooks/useLogout";
4 |
5 | const LogoutButton = () => {
6 | const { loading, logout } = useLogout();
7 | return (
8 |
9 | {!loading ? (
10 |
14 | ) : (
15 |
16 | )}
17 |
18 | );
19 | };
20 |
21 | export default LogoutButton;
22 |
--------------------------------------------------------------------------------
/backend/models/message.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const messageSchema = new mongoose.Schema({
4 | senderId: {
5 | type: mongoose.Schema.Types.ObjectId,
6 | ref: "User",
7 | required: true
8 | },
9 | receiverId: {
10 | type: mongoose.Schema.Types.ObjectId,
11 | ref: "User",
12 | required: true
13 | },
14 | message: {
15 | type: String,
16 | required: true
17 | }
18 | // createdAt, updatedAt
19 | }, { timestamps: true });
20 |
21 | const Message = mongoose.model("Message", messageSchema);
22 |
23 | export default Message;
--------------------------------------------------------------------------------
/backend/utils/generateToken.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 |
3 | const generateTokenAndSetCookie = (userId, res) => {
4 | const token = jwt.sign({ userId }, process.env.JWT_SECRET, {
5 | expiresIn: "15d"
6 | });
7 |
8 | res.cookie("jwt", token, {
9 | maxAge: 15 * 24 * 60 * 60 * 1000, // 15 days (in milliseconds)
10 | httpOnly: true, // Prevents XSS attacks cross-site scripting attacks
11 | sameSite: "strict", // CSRF attacks cross-site request forgery attacks
12 | secure: process.env.NODE_ENV !== "development" // HTTPS
13 | });
14 | };
15 |
16 | export default generateTokenAndSetCookie;
--------------------------------------------------------------------------------
/frontend/src/context/AuthContext.jsx:
--------------------------------------------------------------------------------
1 | import { useState, createContext, useContext } from "react";
2 |
3 | export const AuthContext = createContext();
4 |
5 | // eslint-disable-next-line react-refresh/only-export-components
6 | export const useAuthContext = () => {
7 | return useContext(AuthContext);
8 | };
9 |
10 | export const AuthContextProvider = ({ children }) => {
11 | const [authUser, setAuthUser] = useState(
12 | JSON.parse(localStorage.getItem("chat-user")) || null
13 | );
14 |
15 | return (
16 |
17 | {children}
18 |
19 | );
20 | };
21 |
--------------------------------------------------------------------------------
/backend/models/conversation.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const conversationSchema = new mongoose.Schema(
4 | {
5 | participants: [
6 | {
7 | type: mongoose.Schema.Types.ObjectId,
8 | ref: "User",
9 | },
10 | ],
11 | messages: [{
12 | type: mongoose.Schema.Types.ObjectId,
13 | ref: "Message",
14 | default: [],
15 | },
16 | ],
17 | },
18 | {
19 | timestamps: true
20 | }
21 | );
22 |
23 | const Conversation = mongoose.model("Conversation", conversationSchema);
24 |
25 | export default Conversation;
--------------------------------------------------------------------------------
/frontend/src/main.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import ReactDOM from "react-dom/client";
3 | import App from "./App.jsx";
4 | import "./index.css";
5 | import { BrowserRouter } from "react-router-dom";
6 | import { AuthContextProvider } from "./context/AuthContext.jsx";
7 | import { SocketContextProvider } from "./context/SocketContext.jsx";
8 |
9 | ReactDOM.createRoot(document.getElementById("root")).render(
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 | );
20 |
--------------------------------------------------------------------------------
/backend/models/user.model.js:
--------------------------------------------------------------------------------
1 | import mongoose from "mongoose";
2 |
3 | const userSchema = new mongoose.Schema({
4 | fullName: {
5 | type: String,
6 | required: true,
7 | },
8 | username: {
9 | type: String,
10 | required: true,
11 | unique: true,
12 | },
13 | password: {
14 | type: String,
15 | required: true,
16 | minlength: 6,
17 | },
18 | gender: {
19 | type: String,
20 | required: true,
21 | enum: ["male", "female"],
22 | },
23 | profilePic: {
24 | type: String,
25 | default: "",
26 | },
27 | }, { timestamps: true });
28 |
29 | const User = mongoose.model("User", userSchema);
30 |
31 | export default User;
--------------------------------------------------------------------------------
/frontend/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | env: { browser: true, es2020: true },
4 | extends: [
5 | 'eslint:recommended',
6 | 'plugin:react/recommended',
7 | 'plugin:react/jsx-runtime',
8 | 'plugin:react-hooks/recommended',
9 | ],
10 | ignorePatterns: ['dist', '.eslintrc.cjs'],
11 | parserOptions: { ecmaVersion: 'latest', sourceType: 'module' },
12 | settings: { react: { version: '18.2' } },
13 | plugins: ['react-refresh'],
14 | rules: {
15 | 'react/jsx-no-target-blank': 'off',
16 | 'react-refresh/only-export-components': [
17 | 'warn',
18 | { allowConstantExport: true },
19 | ],
20 | 'react/prop-types': "off",
21 | "react/jsx-uses-react": "error",
22 | "react/jsx-uses-vars": "error"
23 | },
24 | }
25 |
--------------------------------------------------------------------------------
/frontend/src/components/skeletons/MessageSkeletons.jsx:
--------------------------------------------------------------------------------
1 | const MessageSkeleton = () => {
2 | return (
3 | <>
4 |
11 |
17 | >
18 | );
19 | };
20 | export default MessageSkeleton;
21 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "transparent-chat-app-socket.io",
3 | "version": "1.0.0",
4 | "description": "",
5 | "main": "server.js",
6 | "scripts": {
7 | "server": "nodemon backend/server.js",
8 | "start": "node backend/server.js",
9 | "build": "npm install && npm install --prefix frontend && npm run build --prefix frontend"
10 | },
11 | "type": "module",
12 | "keywords": [],
13 | "author": "",
14 | "license": "ISC",
15 | "dependencies": {
16 | "bcryptjs": "^2.4.3",
17 | "cookie-parser": "^1.4.6",
18 | "dotenv": "^16.4.2",
19 | "express": "^4.18.2",
20 | "jsonwebtoken": "^9.0.2",
21 | "mongoose": "^8.1.1",
22 | "nodemon": "^3.0.3",
23 | "react-hot-toast": "^2.4.1",
24 | "redis": "^4.6.15",
25 | "socket.io": "^4.7.4"
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/backend/db/connectToRedis.js:
--------------------------------------------------------------------------------
1 | import { createClient } from 'redis';
2 |
3 | const connectToRedis = async () => {
4 | try {
5 | const client = createClient({
6 | password: process.env.REDIS_PASSWORD,
7 | socket: {
8 | host: process.env.REDIS_HOST,
9 | port: process.env.REDIS_PORT
10 | }
11 | });
12 |
13 | await client.connect(); // Connect to the Redis client
14 | console.log("Connected to Redis");
15 |
16 | return client; // Return the connected client
17 | } catch (error) {
18 | console.log("Error connecting to Redis:", error.message);
19 | throw error; // Rethrow the error to handle it in the server startup
20 | }
21 | }
22 |
23 | export default connectToRedis;
24 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useListenMessages.js:
--------------------------------------------------------------------------------
1 | import { useEffect } from "react";
2 |
3 | import { useSocketContext } from "../context/SocketContext";
4 | import useConversation from "../zustand/useConversation";
5 | import notificationSound from "../assets/sounds/notification.mp3"
6 |
7 | const useListenMessages = () => {
8 | const { socket } = useSocketContext();
9 | const { messages, setMessages } = useConversation();
10 |
11 | useEffect(() => {
12 | socket?.on("newMessage", (newMessage) => {
13 | newMessage.shouldShake = true;
14 | const sound = new Audio(notificationSound);
15 | sound.play();
16 | setMessages([...messages, newMessage]);
17 | })
18 | return () => socket?.off("newMessage")
19 | }, [socket, setMessages, messages])
20 | }
21 |
22 | export default useListenMessages
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/Conversations.jsx:
--------------------------------------------------------------------------------
1 | import React from "react";
2 | import Conversation from "./Conversation.jsx";
3 | import useGetConversations from "./../../hooks/useGetConversations.js";
4 | import { getRandomEmoji } from "../../utils/emoji.js";
5 |
6 | const Conversations = () => {
7 | const { loading, conversations } = useGetConversations();
8 | return (
9 |
10 | {conversations.map((conversation, idx) => (
11 |
17 | ))}
18 | {loading ? : null}
19 |
20 | );
21 | };
22 |
23 | export default Conversations;
24 |
--------------------------------------------------------------------------------
/frontend/src/utils/emoji.js:
--------------------------------------------------------------------------------
1 | export const funEmojis = [
2 | "👾",
3 | "⭐",
4 | "🌟",
5 | "🎉",
6 | "🎊",
7 | "🎈",
8 | "🎁",
9 | "🎂",
10 | "🎄",
11 | "🎃",
12 | "🎗",
13 | "🎟",
14 | "🎫",
15 | "🎖",
16 | "🏆",
17 | "🏅",
18 | "🥇",
19 | "🥈",
20 | "🥉",
21 | "⚽",
22 | "🏀",
23 | "🏈",
24 | "⚾",
25 | "🎾",
26 | "🏐",
27 | "🏉",
28 | "🎱",
29 | "🏓",
30 | "🏸",
31 | "🥅",
32 | "🏒",
33 | "🏑",
34 | "🏏",
35 | "⛳",
36 | "🏹",
37 | "🎣",
38 | "🥊",
39 | "🥋",
40 | "🎽",
41 | "⛸",
42 | "🥌",
43 | "🛷",
44 | "🎿",
45 | "⛷",
46 | "🏂",
47 | "🏋️",
48 | "🤼",
49 | "🤸",
50 | "🤺",
51 | "⛹️",
52 | "🤾",
53 | "🏌️",
54 | "🏇",
55 | "🧘",
56 | ];
57 |
58 | export const getRandomEmoji = () => {
59 | return funEmojis[Math.floor(Math.random() * funEmojis.length)];
60 | };
--------------------------------------------------------------------------------
/frontend/src/hooks/useGetConversations.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import toast from 'react-hot-toast';
3 |
4 | const useGetConversations = () => {
5 | const [loading, setLoading] = useState(false);
6 | const [conversations, setConversations] = useState([]);
7 |
8 | useEffect(() => {
9 | const getConversations = async () => {
10 | setLoading(true);
11 | try {
12 | const res = await fetch("/api/users");
13 | const data = await res.json();
14 | if (data.error) {
15 | throw new Error(data.error);
16 | }
17 | setConversations(data);
18 | } catch (error) {
19 | toast.error(error.message);
20 | } finally {
21 | setLoading(false);
22 | }
23 | }
24 | getConversations();
25 | }, []);
26 | return { loading, conversations };
27 | }
28 |
29 | export default useGetConversations;
--------------------------------------------------------------------------------
/frontend/src/hooks/useLogout.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import { useAuthContext } from "../context/AuthContext"
3 | import toast from 'react-hot-toast';
4 |
5 | const useLogout = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { setAuthUser } = useAuthContext();
8 |
9 | const logout = async () => {
10 | setLoading(true);
11 | try {
12 | const res = await fetch("/api/auth/logout", {
13 | method: "POST",
14 | headers: { "Content-Type": "applicaion/json" }
15 | })
16 | const data = await res.json();
17 | if (data.error) {
18 | throw new Error(data.error);
19 | }
20 | localStorage.removeItem("chat-user");
21 | setAuthUser(null);
22 | } catch (error) {
23 | toast.error(error.message);
24 | } finally {
25 | setLoading(false);
26 | }
27 | }
28 | return { loading, logout };
29 | }
30 |
31 | export default useLogout
--------------------------------------------------------------------------------
/backend/middleware/protectRoute.js:
--------------------------------------------------------------------------------
1 | import jwt from "jsonwebtoken";
2 | import User from "../models/user.model.js";
3 |
4 | const protectRoute = async (req, res, next) => {
5 | try {
6 | const token = req.cookies.jwt;
7 |
8 | if (!token) {
9 | return res.status(401).json({ error: "Unauthorized - No Token Provided" });
10 | }
11 |
12 | const decoded = jwt.verify(token, process.env.JWT_SECRET);
13 |
14 | if (!decoded) {
15 | return res.status(401).json({ error: "Unauthorized - Invalid Token" });
16 | }
17 |
18 | const user = await User.findById(decoded.userId).select("-password");
19 |
20 | if (!user) {
21 | return res.status(404).json({ error: "User not found" });
22 | }
23 |
24 | req.user = user;
25 | next();
26 | } catch (error) {
27 | console.log("Error in protectRoute middleware: ", error.message);
28 | res.status(500).json({ error: "Internal server error" });
29 | }
30 | }
31 |
32 | export default protectRoute;
--------------------------------------------------------------------------------
/frontend/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "frontend",
3 | "private": true,
4 | "version": "0.0.0",
5 | "type": "module",
6 | "scripts": {
7 | "dev": "vite",
8 | "build": "vite build",
9 | "lint": "eslint . --ext js,jsx --report-unused-disable-directives --max-warnings 0",
10 | "preview": "vite preview"
11 | },
12 | "dependencies": {
13 | "react": "^18.2.0",
14 | "react-dom": "^18.2.0",
15 | "react-hot-toast": "^2.4.1",
16 | "react-icons": "^5.1.0",
17 | "react-router-dom": "^6.22.3",
18 | "socket.io-client": "^4.7.5",
19 | "transparent-chat-app-socket.io": "file:..",
20 | "zustand": "^4.5.4"
21 | },
22 | "devDependencies": {
23 | "@types/react": "^18.2.55",
24 | "@types/react-dom": "^18.2.19",
25 | "@vitejs/plugin-react": "^4.2.1",
26 | "autoprefixer": "^10.4.17",
27 | "daisyui": "^4.7.0",
28 | "eslint": "^8.56.0",
29 | "eslint-plugin-react": "^7.33.2",
30 | "eslint-plugin-react-hooks": "^4.6.0",
31 | "eslint-plugin-react-refresh": "^0.4.5",
32 | "postcss": "^8.4.35",
33 | "tailwindcss": "^3.4.1",
34 | "vite": "^5.1.0"
35 | }
36 | }
37 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024 Kriti Raj
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.
22 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useGetMessages.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import toast from 'react-hot-toast';
3 | import useConversation from '../zustand/useConversation';
4 |
5 | const useGetMessages = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { messages, setMessages, selectedConversation } = useConversation();
8 | useEffect(() => {
9 | const getMessages = async () => {
10 | setLoading(true);
11 | try {
12 | const res = await fetch(`/api/messages/${selectedConversation._id}`);
13 | const data = await res.json();
14 | if (data.error) {
15 | throw new Error();
16 | }
17 | setMessages(data);
18 | } catch (error) {
19 | toast.error(error.message);
20 | } finally {
21 | setLoading(false);
22 | }
23 | }
24 | if (selectedConversation?._id) getMessages();
25 | }, [selectedConversation?._id, setMessages])
26 | return { messages, loading };
27 | }
28 |
29 | export default useGetMessages
--------------------------------------------------------------------------------
/frontend/src/App.jsx:
--------------------------------------------------------------------------------
1 | import { Navigate, Route, Routes } from "react-router-dom";
2 | import Home from "./pages/home/Home.jsx";
3 | import Login from "./pages/login/Login.jsx";
4 | import SignUp from "./pages/signup/SignUp.jsx";
5 | import { Toaster } from "react-hot-toast";
6 | import { useAuthContext } from "./context/AuthContext.jsx";
7 |
8 | const App = () => {
9 | const { authUser } = useAuthContext();
10 | return (
11 |
12 |
13 | : }
16 | />
17 | : }
20 | />
21 | : }
24 | />
25 | : }
28 | />
29 |
30 |
31 |
32 | );
33 | };
34 |
35 | export default App;
36 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useSendMessage.js:
--------------------------------------------------------------------------------
1 | import { useState } from 'react'
2 | import toast from 'react-hot-toast';
3 | import useConversation from '../zustand/useConversation';
4 |
5 | const useSendMessage = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { messages, setMessages, selectedConversation } = useConversation();
8 |
9 | const sendMessage = async (message) => {
10 | setLoading(true);
11 | try {
12 | const res = await fetch(`/api/messages/send/${selectedConversation._id}`, {
13 | method: 'POST',
14 | headers: { "Content-Type": "application/json" },
15 | body: JSON.stringify({ message })
16 | })
17 | const data = await res.json();
18 | if (data.error) {
19 | throw new Error(data.error);
20 | }
21 | setMessages([...messages, data]);
22 | } catch (error) {
23 | toast.error(error.message);
24 | } finally {
25 | setLoading(false);
26 | }
27 | }
28 | return { sendMessage, loading }
29 | }
30 |
31 | export default useSendMessage
--------------------------------------------------------------------------------
/frontend/src/index.css:
--------------------------------------------------------------------------------
1 | @tailwind base;
2 | @tailwind components;
3 | @tailwind utilities;
4 |
5 | body {
6 | background: linear-gradient(rgba(0, 0, 0, 0.2), rgba(0, 0, 0, 0.2)),
7 | url(/bg.jpg);
8 | background-repeat: no-repeat;
9 | background-size: cover;
10 | background-position: center;
11 | }
12 |
13 | /* dark mode looking scrollbar */
14 | ::-webkit-scrollbar {
15 | width: 8px;
16 | }
17 |
18 | ::-webkit-scrollbar-track {
19 | background: #555;
20 | }
21 |
22 | ::-webkit-scrollbar-thumb {
23 | background: #121212;
24 | border-radius: 5px;
25 | }
26 |
27 | ::-webkit-scrollbar-thumb:hover {
28 | background: #242424;
29 | }
30 |
31 | /* SHAKE ANIMATION ON HORIZONTAL DIRECTION */
32 | .shake {
33 | animation: shake 0.82s cubic-bezier(0.36, 0.07, 0.19, 0.97) 0.2s both;
34 | transform: translate3d(0, 0, 0);
35 | backface-visibility: hidden;
36 | perspective: 1000px;
37 | }
38 |
39 | @keyframes shake {
40 |
41 | 10%,
42 | 90% {
43 | transform: translate3d(-1px, 0, 0);
44 | }
45 |
46 | 20%,
47 | 80% {
48 | transform: translate3d(2px, 0, 0);
49 | }
50 |
51 | 30%,
52 | 50%,
53 | 70% {
54 | transform: translate3d(-4px, 0, 0);
55 | }
56 |
57 | 40%,
58 | 60% {
59 | transform: translate3d(4px, 0, 0);
60 | }
61 | }
--------------------------------------------------------------------------------
/backend/socket/socket.js:
--------------------------------------------------------------------------------
1 | import { Server } from "socket.io";
2 | import http from "http";
3 | import express from "express";
4 |
5 | const app = express();
6 |
7 | const server = http.createServer(app);
8 | const io = new Server(server, {
9 | cors: {
10 | origin: ["http://localhost:5000"],
11 | methods: ["GET", "POST"]
12 | }
13 | });
14 |
15 | export const getReceiverSocketId = (receiverId) => {
16 | return userSocketMap[receiverId];
17 | }
18 |
19 | const userSocketMap = {}; // {userId: socketId}
20 |
21 |
22 | io.on("connection", (socket) => {
23 | // console.log("a user connected", socket.id);
24 |
25 | const userId = socket.handshake.query.userId;
26 | if (userId != "undefined") userSocketMap[userId] = socket.id;
27 |
28 | // io.emit() is used to send events to all connected clients
29 | io.emit("getOnlineUsers", Object.keys(userSocketMap));
30 |
31 | // socket.on() is used to listen to the events. can be used both on client and server side
32 | socket.on("disconnect", () => {
33 | // console.log("user disconnected", socket.id);
34 | delete userSocketMap[userId];
35 | io.emit("getOnlineUsers", Object.keys(userSocketMap));
36 | })
37 | })
38 |
39 | export { app, io, server };
--------------------------------------------------------------------------------
/frontend/src/components/messages/Messages.jsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useRef } from "react";
2 | import Message from "./Message.jsx";
3 | import useGetMessages from "../../hooks/useGetMessages.js";
4 | import MessageSkeleton from "../skeletons/MessageSkeletons.jsx";
5 | import useListenMessages from "../../hooks/useListenMessages.js";
6 |
7 | const Messages = () => {
8 | const { messages, loading } = useGetMessages();
9 | useListenMessages();
10 |
11 | const lastMessageRef = useRef();
12 |
13 | useEffect(() => {
14 | setTimeout(() => {
15 | lastMessageRef.current?.scrollIntoView({ behavior: "smooth" });
16 | }, 100);
17 | }, [messages]);
18 |
19 | return (
20 |
21 | {!loading &&
22 | messages.length > 0 &&
23 | messages.map((message) => (
24 |
25 |
26 |
27 | ))}
28 | {loading && [...Array(3)].map((_, idx) =>
)}
29 | {!loading && messages.length === 0 && (
30 |
Send a message to start the conversation
31 | )}
32 |
33 | );
34 | };
35 |
36 | export default Messages;
37 |
--------------------------------------------------------------------------------
/frontend/src/pages/signup/GenderCheckbox.jsx:
--------------------------------------------------------------------------------
1 | const GenderCheckbox = ({ onCheckboxChange, selectedGender }) => {
2 | return (
3 |
36 | );
37 | };
38 |
39 | export default GenderCheckbox;
40 |
--------------------------------------------------------------------------------
/frontend/src/components/messages/MessageInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { BsSend } from "react-icons/bs";
3 | import useSendMessage from "../../hooks/useSendMessage";
4 |
5 | const MessageInput = () => {
6 | const [message, setMessage] = useState("");
7 | const { sendMessage, loading } = useSendMessage();
8 | const handleSubmit = async (e) => {
9 | e.preventDefault();
10 | if (!message) return;
11 | await sendMessage(message);
12 | setMessage("");
13 | };
14 | return (
15 |
36 | );
37 | };
38 |
39 | export default MessageInput;
40 |
--------------------------------------------------------------------------------
/frontend/src/context/SocketContext.jsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, useEffect, useState } from "react";
2 | import { useAuthContext } from "./AuthContext";
3 | import io from "socket.io-client";
4 |
5 | const SocketContext = createContext();
6 |
7 | export const useSocketContext = () => {
8 | return useContext(SocketContext);
9 | };
10 |
11 | export const SocketContextProvider = ({ children }) => {
12 | const [socket, setSocket] = useState(null);
13 | const [onlineUsers, setOnlineUsers] = useState([]);
14 | const { authUser } = useAuthContext();
15 |
16 | useEffect(() => {
17 | if (authUser) {
18 | const socket = io("https://messenger-web-app.onrender.com", {
19 | query: {
20 | userId: authUser._id,
21 | },
22 | });
23 |
24 | setSocket(socket);
25 |
26 | // socket.on() is used to listen to the events. can be used both on client and server side
27 | socket.on("getOnlineUsers", (users) => {
28 | setOnlineUsers(users);
29 | });
30 |
31 | return () => socket.close();
32 | } else {
33 | if (socket) {
34 | socket.close();
35 | setSocket(null);
36 | }
37 | }
38 | }, [authUser]);
39 |
40 | return (
41 |
42 | {children}
43 |
44 | );
45 | };
46 |
--------------------------------------------------------------------------------
/frontend/src/components/messages/Message.jsx:
--------------------------------------------------------------------------------
1 | import { useAuthContext } from "../../context/AuthContext";
2 | import { extractTime } from "../../utils/extractTime";
3 | import useConversation from "../../zustand/useConversation";
4 |
5 | const Message = ({ message }) => {
6 | const { authUser } = useAuthContext();
7 | const { selectedConversation } = useConversation();
8 | const fromMe = message.senderId === authUser._id;
9 | const formattedTime = extractTime(message.createdAt);
10 | const chatClassName = fromMe ? "chat-end" : "chat-start";
11 | const profilePic = fromMe
12 | ? authUser.profilePic
13 | : selectedConversation.profilePic;
14 | const bubbleBgColor = fromMe ? "bg-blue-500" : "";
15 | const shakeClass = message.shouldShake ? "shake" : "";
16 |
17 | return (
18 |
19 |
20 |
21 |

22 |
23 |
24 |
27 | {/* Hi! What is Upp? */}
28 | {message.message}
29 |
30 |
31 | {formattedTime}
32 |
33 |
34 | );
35 | };
36 |
37 | export default Message;
38 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/Conversation.jsx:
--------------------------------------------------------------------------------
1 | import { useSocketContext } from "../../context/SocketContext";
2 | import useConversation from "../../zustand/useConversation";
3 |
4 | const Conversation = ({ conversation, lastIdx, emoji }) => {
5 | const { selectedConversation, setSelectedConversation } = useConversation();
6 | const isSelected = selectedConversation?._id === conversation._id;
7 | const { onlineUsers } = useSocketContext();
8 | const isOnline = onlineUsers.includes(conversation._id);
9 |
10 | return (
11 | <>
12 | setSelectedConversation(conversation)}
17 | >
18 |
19 |
20 |

21 |
22 |
23 |
24 |
25 |
26 |
{conversation.fullName}
27 |
{emoji}
28 |
29 |
30 |
31 | {!lastIdx && }
32 | >
33 | );
34 | };
35 |
36 | export default Conversation;
37 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useLogin.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react"
2 | import toast from "react-hot-toast";
3 | import { useAuthContext } from "../context/AuthContext";
4 |
5 | const useLogin = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { setAuthUser } = useAuthContext();
8 |
9 | const login = async (username, password) => {
10 |
11 | const success = handleInputErrors(username, password);
12 | if (!success) return;
13 | setLoading(true);
14 | try {
15 | const res = await fetch("/api/auth/login", {
16 | method: "POST",
17 | headers: { "Content-Type": "application/json" },
18 | body: JSON.stringify({ username, password })
19 | });
20 | const data = await res.json();
21 | if (data.error) {
22 | throw new Error(data.error);
23 | }
24 | localStorage.setItem("chat-user", JSON.stringify(data));
25 | setAuthUser(data);
26 | } catch (error) {
27 | toast.error(error.message);
28 | } finally {
29 | setLoading(false);
30 | }
31 | }
32 | return { loading, login };
33 | }
34 |
35 | export default useLogin
36 |
37 | function handleInputErrors(username, password) {
38 | if (!username || !password) {
39 | toast.error("Please fill all the fields");
40 | return false;
41 | }
42 |
43 | return true;
44 | }
45 |
--------------------------------------------------------------------------------
/backend/controller/user.controller.js:
--------------------------------------------------------------------------------
1 | import User from "../models/user.model.js";
2 |
3 | export const getUsersForSidebar = async (req, res) => {
4 | try {
5 | const loggedInUserId = req.user._id;
6 | const client = req.app.locals.redisClient; // Get the Redis client from app locals
7 |
8 | if (!client) {
9 | console.log("Redis client is not initialized");
10 | return res.status(500).json({ error: "Internal Server Error" });
11 | }
12 |
13 | // Check if data is already cached in Redis
14 | const cacheKey = `usersForSidebar:${loggedInUserId}`;
15 | const cachedData = await client.get(cacheKey);
16 | if (cachedData) {
17 | // console.log('Data from redis');
18 | return res.status(200).json(JSON.parse(cachedData)); // Parse cached data before sending response
19 | }
20 |
21 | // Fetch users from MongoDB excluding the logged-in user
22 | const filteredUsers = await User.find({ _id: { $ne: loggedInUserId } }).select("-password");
23 |
24 | // Store data in Redis with an expiration time
25 | await client.set(cacheKey, JSON.stringify(filteredUsers), {
26 | EX: 300 // Set key with an expiration of 5 minutes
27 | });
28 |
29 | res.status(200).json(filteredUsers);
30 | } catch (error) {
31 | console.error("Error in getUsersForSidebar: ", error.message);
32 | res.status(500).json({ error: "Internal Server Error" });
33 | }
34 | }
35 |
--------------------------------------------------------------------------------
/frontend/src/components/sidebar/SearchInput.jsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from "react";
2 | import { FaSearch } from "react-icons/fa";
3 | import useConversation from "../../zustand/useConversation";
4 | import useGetConversations from "../../hooks/useGetConversations";
5 | import toast from "react-hot-toast";
6 |
7 | const SearchInput = () => {
8 | const [search, setSearch] = useState("");
9 | const { setSelectedConversation } = useConversation();
10 | const { conversations } = useGetConversations();
11 | const handleSubmit = (e) => {
12 | e.preventDefault();
13 | if (!search) return;
14 | if (search.length < 3) {
15 | return toast.error("Search term must be at least 3 characters long");
16 | }
17 |
18 | const conversation = conversations.find((c) =>
19 | c.fullName.toLowerCase().includes(search.toLowerCase())
20 | );
21 |
22 | if (conversation) {
23 | setSelectedConversation(conversation);
24 | setSearch("");
25 | } else toast.error("No such user found!");
26 | };
27 | return (
28 |
40 | );
41 | };
42 |
43 | export default SearchInput;
44 |
--------------------------------------------------------------------------------
/frontend/src/components/messages/MessageContainer.jsx:
--------------------------------------------------------------------------------
1 | import Messages from "./Messages";
2 | import MessageInput from "./MessageInput";
3 | import { TiMessages } from "react-icons/ti";
4 | import useConversation from "../../zustand/useConversation";
5 | import { useEffect } from "react";
6 | import { useAuthContext } from "../../context/AuthContext";
7 |
8 | const MessageContainer = () => {
9 | const { selectedConversation, setSelectedConversation } = useConversation();
10 | useEffect(() => {
11 | // cleanup function (unmounts)
12 | return () => setSelectedConversation(null);
13 | }, [setSelectedConversation]);
14 | return (
15 |
16 | {!selectedConversation ? (
17 |
18 | ) : (
19 | <>
20 |
21 | To:{" "}
22 |
23 | {selectedConversation.fullName}
24 |
25 |
26 |
27 |
28 | >
29 | )}
30 |
31 | );
32 | };
33 |
34 | const NoChatSelected = () => {
35 | const { authUser } = useAuthContext();
36 | return (
37 |
38 |
39 |
Welcome 👋 {authUser.fullName} ❄️
40 |
Select a chat to start messaging
41 |
42 |
43 |
44 | );
45 | };
46 |
47 | export default MessageContainer;
48 |
--------------------------------------------------------------------------------
/backend/server.js:
--------------------------------------------------------------------------------
1 | // Imports
2 | import path from "path";
3 | import express from "express";
4 | import dotenv from "dotenv";
5 | import cookieParser from "cookie-parser";
6 |
7 | import authRoutes from "./routes/auth.routes.js";
8 | import messageRoutes from "./routes/message.routes.js";
9 | import userRoutes from "./routes/user.routes.js";
10 |
11 | import connectToRedis from "./db/connectToRedis.js";
12 | import connectToMongoDB from "./db/connectToMongoDB.js";
13 | import { app, server } from "./socket/socket.js";
14 |
15 | dotenv.config();
16 |
17 | const __dirname = path.resolve();
18 |
19 | // PORT should be assigned after calling dotenv.config()
20 | const PORT = process.env.PORT || 3000;
21 |
22 | // Middlewares
23 | app.use(express.json()); // to parse the incoming request with JSON payloads (from req.body)
24 | app.use(cookieParser());
25 |
26 | app.use("/api/auth", authRoutes);
27 | app.use("/api/messages", messageRoutes);
28 | app.use("/api/users", userRoutes);
29 |
30 | app.use(express.static(path.join(__dirname, "/frontend/dist")));
31 |
32 | app.get('*', (req, res) => {
33 | res.sendFile(path.join(__dirname, "frontend", "dist", "index.html"));
34 | });
35 |
36 | // Function to start the server
37 | const startServer = async () => {
38 | try {
39 | await connectToMongoDB(); // Connect to MongoDB
40 | const redisClient = await connectToRedis(); // Connect to Redis
41 |
42 | app.locals.redisClient = redisClient; // Store the Redis client in app locals for later use
43 |
44 | server.listen(PORT, () => {
45 | console.log(`Server Running on port ${PORT}`);
46 | });
47 | } catch (error) {
48 | console.error('Failed to start server:', error.message);
49 | }
50 | };
51 |
52 | startServer();
53 |
--------------------------------------------------------------------------------
/frontend/src/hooks/useSignup.js:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import toast from "react-hot-toast";
3 | import { useAuthContext } from "../context/AuthContext";
4 |
5 | const useSignup = () => {
6 | const [loading, setLoading] = useState(false);
7 | const { setAuthUser } = useAuthContext();
8 |
9 | const signup = async ({ fullName, username, password, confirmPassword, gender }) => {
10 | const success = handleInputErrors({ fullName, username, password, confirmPassword, gender });
11 | if (!success) return;
12 | setLoading(true);
13 | try {
14 | const res = await fetch("/api/auth/signup", {
15 | method: "POST",
16 | headers: { "Content-Type": "application/json" },
17 | body: JSON.stringify({ fullName, username, password, confirmPassword, gender })
18 | });
19 |
20 |
21 | const data = await res.json();
22 | if (data.error) {
23 | throw new Error(data.error);
24 | }
25 |
26 | localStorage.setItem("chat-user", JSON.stringify(data));
27 | setAuthUser(data);
28 |
29 | toast.success('Signup successful');
30 | } catch (error) {
31 | toast.error(error.message);
32 | } finally {
33 | setLoading(false);
34 | }
35 | };
36 | return { loading, signup };
37 | };
38 |
39 | export default useSignup;
40 |
41 | function handleInputErrors({ fullName, username, password, confirmPassword, gender }) {
42 | if (!fullName || !username || !password || !confirmPassword || !gender) {
43 | toast.error("Please fill all the fields");
44 | return false;
45 | }
46 |
47 | if (password !== confirmPassword) {
48 | toast.error("Passwords do not match");
49 | return false;
50 | }
51 |
52 | if (password.length < 6) {
53 | toast.error("Password must be at least 6 characters");
54 | return false;
55 | }
56 |
57 | return true;
58 | }
59 |
--------------------------------------------------------------------------------
/frontend/src/pages/login/Login.jsx:
--------------------------------------------------------------------------------
1 | import { useState } from "react";
2 | import { Link } from "react-router-dom";
3 | import useLogin from "../../hooks/useLogin";
4 |
5 | const Login = () => {
6 | const [username, setUsername] = useState("");
7 | const [password, setpassword] = useState("");
8 |
9 | const { loading, login } = useLogin();
10 |
11 | const handleSubmit = async (e) => {
12 | e.preventDefault();
13 | await login(username, password);
14 | };
15 | return (
16 |
17 |
18 |
19 | Login Messenger
20 |
21 |
66 |
67 |
68 | );
69 | };
70 |
71 | export default Login;
72 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | # Messenger Web App
3 |
4 | ## Introduction
5 | Messenger Web App is a real-time chatting application similar to Facebook Messenger. It features user authentication and hashed password storage to ensure secure communication.
6 | ## Demo
7 |
8 | [Messenger Web App](https://messenger-web-app.onrender.com/)
9 | ## Tech Stack
10 |
11 | **Client:** React, TailwindCSS, Daisy UI, Socket.io
12 |
13 | **Server:** Node, Express, MongoDB, Redis, Socket.io, JWT, cookie-parser
14 |
15 |
16 | ## Features
17 |
18 |
19 | - Real-time messaging using Socket.IO
20 | - User authentication with JWT
21 | - Password hashing with bcrypt
22 | - MongoDB for data storage
23 | - Redis for data caching
24 | ## Dependencies
25 |
26 | The project uses the following major dependencies:
27 |
28 | - Vite for the development server and build tool
29 | - React for building the user interface
30 | - Socket.IO for real-time communication
31 | - JWT for authentication
32 | - MongoDB for database management
33 | - bcrypt for password hashing
34 | - Redis for data cahcing
35 | ## Deployment
36 |
37 | To deploy the Messenger Web App, follow these steps:
38 | 1. Clone the repository:
39 | ```bash
40 | git clone https://github.com/kriti-raj/messenger-web-app.git
41 | ```
42 | 2. Navigate to the project directory:
43 | ```bash
44 | cd messenger-web-app
45 | ```
46 | 3. Install the dependencies:
47 | ```bash
48 | npm install
49 | ```
50 | 4. start the development server:
51 | ```bash
52 | npm run dev
53 | ```
54 | This will start the server at `http://localhost:5000`. Open your browser and navigate to this URL to see the application in action.
55 | ## Environment Variables
56 |
57 | To run this project, you will need to add the following environment variables to your .env file
58 |
59 | - `PORT`
60 | - `MONGO_DB_URI`
61 | - `JWT_SECRET`
62 | - `NODE_ENV`
63 | - `REDIS_PASSWORD`
64 | - `REDIS_HOST`
65 | - `REDIS_PORT`
66 | ## Documentation
67 |
68 | The project documentation is currently maintained in this README file. For more detailed information on specific topics, please refer to the respective official documentation of the tools and libraries used.
69 |
70 | ## Examples
71 | To see examples of how to use various features of the Messenger Web App, refer to the source code in the src directory.
72 |
73 | ## Troubleshooting
74 | If you encounter any issues, please check the following:
75 |
76 | Ensure all dependencies are installed correctly by running npm install.
77 | Verify that the development server is running on the correct port (http://localhost:5000).
78 | Check the browser console for any error messages.
79 |
80 | ## License
81 |
82 | [](https://choosealicense.com/licenses/mit/)
83 |
84 | ## Contributing
85 |
86 | We welcome contributions from the community. If you'd like to contribute, please fork the repository and submit a pull request.
87 | ## Feedback
88 |
89 | If you have any feedback, please reach out to us at 6517kritiraj@gmail.com
90 |
--------------------------------------------------------------------------------
/backend/controller/message.controller.js:
--------------------------------------------------------------------------------
1 | import Conversation from "../models/conversation.model.js";
2 | import Message from "../models/message.model.js";
3 | import { getReceiverSocketId, io } from "../socket/socket.js";
4 |
5 | export const sendMessage = async (req, res) => {
6 | try {
7 | const { message } = req.body;
8 | const { id: receiverId } = req.params;
9 | const senderId = req.user._id;
10 |
11 | let conversation = await Conversation.findOne({
12 | participants: { $all: [senderId, receiverId] },
13 | });
14 |
15 | if (!conversation) {
16 | conversation = await Conversation.create({
17 | participants: [senderId, receiverId],
18 | });
19 | }
20 |
21 | const newMessage = new Message({
22 | senderId,
23 | receiverId,
24 | message,
25 | });
26 |
27 | if (newMessage) {
28 | conversation.messages.push(newMessage._id);
29 | }
30 |
31 | // Save conversation and message in parallel
32 | await Promise.all([conversation.save(), newMessage.save()]);
33 |
34 | // SOCKET IO FUNCTIONALITY WILL GO HERE
35 | const receiverSocketId = getReceiverSocketId(receiverId);
36 | if (receiverSocketId) {
37 | io.to(receiverSocketId).emit("newMessage", newMessage);
38 | }
39 |
40 | res.status(201).json(newMessage);
41 | } catch (error) {
42 | console.log("Error in sendMessage controller:", error.message);
43 | res.status(500).json({ error: "Internal Server Error" });
44 | }
45 | };
46 |
47 | export const getMessages = async (req, res) => {
48 | try {
49 | const { id: userToChatId } = req.params;
50 | const senderId = req.user._id;
51 | const client = req.app.locals.redisClient; // Get the Redis client from app locals
52 |
53 | if (!client) {
54 | console.log("Redis client is not initialized");
55 | return res.status(500).json({ error: "Internal Server Error" });
56 | }
57 |
58 | // Check if data is already cached in Redis
59 | const cacheKey = `conversation:${senderId}:${userToChatId}`;
60 | const cachedData = await client.get(cacheKey);
61 | if (cachedData) {
62 | // console.log('Data from redis');
63 | return res.status(200).json(JSON.parse(cachedData)); // Parse cached data before sending response
64 | }
65 |
66 | const conversation = await Conversation.findOne({
67 | participants: { $all: [senderId, userToChatId] },
68 | }).populate("messages");
69 |
70 | if (!conversation) return res.status(200).json([]);
71 |
72 | const messages = conversation.messages;
73 |
74 | // Store data in Redis with an expiration time
75 | await client.set(cacheKey, JSON.stringify(messages), {
76 | EX: 300 // Set key with an expiration of 5 minutes
77 | });
78 |
79 | res.status(200).json(messages);
80 | } catch (error) {
81 | console.log("Error in getMessages controller:", error.message);
82 | res.status(500).json({ error: "Internal Server Error" });
83 | }
84 | };
85 |
--------------------------------------------------------------------------------
/backend/controller/auth.controller.js:
--------------------------------------------------------------------------------
1 | import bcrypt from "bcryptjs";
2 | import User from "../models/user.model.js";
3 | import generateTokenAndSetCookie from "../utils/generateToken.js";
4 |
5 | export const signup = async (req, res) => {
6 | try {
7 | const { fullName, username, password, confirmPassword, gender } = req.body;
8 |
9 | if (password != confirmPassword) {
10 | return res.status(400).json({ error: "Password doesn't match" });
11 | }
12 | const user = await User.findOne({ username });
13 |
14 | if (user) {
15 | return res.status(400).json({ error: "Username already exists" });
16 | }
17 |
18 | // HASH PASSWORD HERE
19 | const salt = await bcrypt.genSalt(10);
20 | const hashedPassword = await bcrypt.hash(password, salt);
21 |
22 | // http://avatar-placeholder.iran.liara.run/
23 |
24 | const boyProfilePic = `https://avatar.iran.liara.run/public/boy?username=${username}`;
25 | const girlProfilePic = `https://avatar.iran.liara.run/public/girl?username=${username}`;
26 |
27 | const newUser = new User({
28 | fullName,
29 | username,
30 | password: hashedPassword,
31 | gender,
32 | profilePic: gender === "male" ? boyProfilePic : girlProfilePic,
33 | });
34 |
35 | if (newUser) {
36 | // Generate JWT token here
37 | generateTokenAndSetCookie(newUser._id, res);
38 | await newUser.save();
39 |
40 | res.status(201).json({
41 | _id: newUser._id,
42 | fullName: newUser.fullName,
43 | username: newUser.username,
44 | profilePic: newUser.profilePic,
45 | });
46 | } else {
47 | res.status(400).json({ error: "Invalid user data" });
48 | }
49 | } catch (error) {
50 | console.log("Error in signup controller", error.message);
51 | res.status(500).json({
52 | error: "Internal Server Error"
53 | });
54 | }
55 | };
56 |
57 | export const login = async (req, res) => {
58 | try {
59 | const { username, password } = req.body;
60 | const user = await User.findOne({ username });
61 | const isPasswordCorrect = await bcrypt.compare(password, user.password || "");
62 |
63 | if (!user || !isPasswordCorrect) {
64 | return res.status(400).json({ error: "Invalid username or password" });
65 | }
66 |
67 | generateTokenAndSetCookie(user._id, res);
68 |
69 | res.status(200).json({
70 | _id: user._id,
71 | fullName: user.fullName,
72 | username: user.username,
73 | profilePic: user.profilePic,
74 | });
75 |
76 | } catch (error) {
77 | console.log("Error in login controller", error.message);
78 | res.status(500).json({
79 | error: "Internal Server Error"
80 | });
81 | }
82 | };
83 |
84 | export const logout = (req, res) => {
85 | try {
86 | res.cookie("jwt", "", { maxAge: 0 });
87 | res.status(200).json({ message: "Logged out successfully" });
88 |
89 | } catch (error) {
90 | console.log("Error in logout controller", error.message);
91 | res.status(500).json({
92 | error: "Internal Server Error"
93 | });
94 | }
95 | };
--------------------------------------------------------------------------------
/frontend/src/pages/signup/SignUp.jsx:
--------------------------------------------------------------------------------
1 | import { Link } from "react-router-dom";
2 | import GenderCheckbox from "./GenderCheckbox";
3 | import { useState } from "react";
4 | import useSignup from "../../hooks/useSignup";
5 |
6 | const SignUp = () => {
7 | const [inputs, setInputs] = useState({
8 | fullName: "",
9 | username: "",
10 | password: "",
11 | confirmPassword: "",
12 | gender: "",
13 | });
14 |
15 | // eslint-disable-next-line no-unused-vars
16 | const { loading, signup } = useSignup();
17 |
18 | const handleCheckboxChange = (gender) => {
19 | setInputs({ ...inputs, gender });
20 | };
21 |
22 | const handleSubmit = async (e) => {
23 | e.preventDefault();
24 | await signup(inputs);
25 | };
26 |
27 | return (
28 |
29 |
30 |
31 | Sign Up ChatApp
32 |
33 |
34 |
119 |
120 |
121 | );
122 | };
123 |
124 | export default SignUp;
125 |
--------------------------------------------------------------------------------