├── .gitignore ├── frontend ├── public │ ├── robots.txt │ ├── 134914.png │ ├── favicon.ico │ ├── logo192.png │ ├── logo512.png │ ├── manifest.json │ └── index.html ├── src │ ├── background.png │ ├── background2.jpeg │ ├── components │ │ ├── messTone.mp3 │ │ ├── Notification.mp3 │ │ ├── styles.css │ │ ├── UserAvatar │ │ │ ├── UserBadgeItem.js │ │ │ └── UserListItem.js │ │ ├── ChatLoading.js │ │ ├── ChatBox.js │ │ ├── miscellaneous │ │ │ ├── ProfileModal.js │ │ │ ├── GroupChatModal.js │ │ │ ├── SideDrawer.js │ │ │ └── UpdateGroupChatModal.js │ │ ├── ScrollableChat.js │ │ ├── Authentication │ │ │ ├── Login.js │ │ │ └── Signup.js │ │ ├── MyChats.js │ │ └── SingleChat.js │ ├── App.js │ ├── index.css │ ├── App.css │ ├── Context │ │ ├── EncrDecr.js │ │ └── ChatProvider.js │ ├── index.js │ ├── Pages │ │ ├── Chatpage.js │ │ └── Homepage.js │ ├── config │ │ └── ChatLogics.js │ └── animations │ │ └── typing.json ├── .gitignore ├── package.json └── README.md ├── screenshorts ├── addrem.PNG ├── newgrp.PNG └── group_notif.PNG ├── backend ├── config │ ├── generateToken.js │ └── db.js ├── routes │ ├── userRoutes.js │ ├── messageRoutes.js │ └── chatRoutes.js ├── models │ ├── messageModel.js │ ├── chatModel.js │ └── userModel.js ├── EncrDecr.js ├── middleware │ ├── errorMiddleware.js │ └── authMiddleware.js ├── controllers │ ├── userControllers.js │ ├── messageControllers.js │ └── chatControllers.js ├── data │ └── data.js └── server.js ├── package.json └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | frontend/node_modules 3 | .env -------------------------------------------------------------------------------- /frontend/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /screenshorts/addrem.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/screenshorts/addrem.PNG -------------------------------------------------------------------------------- /screenshorts/newgrp.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/screenshorts/newgrp.PNG -------------------------------------------------------------------------------- /frontend/public/134914.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/public/134914.png -------------------------------------------------------------------------------- /frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/public/favicon.ico -------------------------------------------------------------------------------- /frontend/public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/public/logo192.png -------------------------------------------------------------------------------- /frontend/public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/public/logo512.png -------------------------------------------------------------------------------- /frontend/src/background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/src/background.png -------------------------------------------------------------------------------- /screenshorts/group_notif.PNG: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/screenshorts/group_notif.PNG -------------------------------------------------------------------------------- /frontend/src/background2.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/src/background2.jpeg -------------------------------------------------------------------------------- /frontend/src/components/messTone.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/src/components/messTone.mp3 -------------------------------------------------------------------------------- /frontend/src/components/Notification.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NitinVadadoriyaa/JerryChat/HEAD/frontend/src/components/Notification.mp3 -------------------------------------------------------------------------------- /frontend/src/components/styles.css: -------------------------------------------------------------------------------- 1 | .messages { 2 | display: flex; 3 | flex-direction: column; 4 | overflow-y: scroll; 5 | scrollbar-width: none; 6 | } 7 | -------------------------------------------------------------------------------- /backend/config/generateToken.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | 3 | const generateToken = (id) => { 4 | return jwt.sign({ id }, process.env.JWT_SECRET, { 5 | expiresIn: "30d", 6 | }); 7 | }; 8 | 9 | module.exports = generateToken; 10 | -------------------------------------------------------------------------------- /frontend/src/App.js: -------------------------------------------------------------------------------- 1 | import "./App.css"; 2 | import Homepage from "./Pages/Homepage"; 3 | import { Route } from "react-router-dom"; 4 | import Chatpage from "./Pages/Chatpage"; 5 | 6 | function App() { 7 | return ( 8 |
9 | 10 | 11 |
12 | ); 13 | } 14 | 15 | export default App; 16 | -------------------------------------------------------------------------------- /frontend/.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 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /frontend/src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /backend/routes/userRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { 3 | registerUser, 4 | authUser, 5 | allUsers, 6 | } = require("../controllers/userControllers"); 7 | const router = express.Router(); 8 | const { protect } = require("../middleware/authMiddleware"); 9 | 10 | router.route("/").get(protect, allUsers); 11 | router.route("/").post(registerUser); 12 | router.post("/login", authUser); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /backend/routes/messageRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { protect } = require("../middleware/authMiddleware"); 3 | const { route } = require("./userRoutes"); 4 | const { 5 | sendMessage, 6 | allMessages, 7 | } = require("../controllers/messageControllers"); 8 | 9 | const router = express.Router(); 10 | 11 | router.route("/").post(protect, sendMessage); 12 | router.route("/:chatId").get(protect, allMessages); 13 | 14 | module.exports = router; 15 | -------------------------------------------------------------------------------- /backend/config/db.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const connectDB = async () => { 4 | try { 5 | const conn = await mongoose.connect(process.env.MONGO_URI, { 6 | useNewUrlParser: true, 7 | useUnifiedTopology: true, 8 | }); 9 | 10 | console.log(`MongoDB Connected: ${conn.connection.host}`); 11 | } catch (error) { 12 | console.log(`Error : ${error.message}`); 13 | process.exit(); 14 | } 15 | }; 16 | 17 | module.exports = connectDB; 18 | -------------------------------------------------------------------------------- /backend/models/messageModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | 3 | const messageModel = mongoose.Schema( 4 | { 5 | sender: { type: mongoose.Schema.Types.ObjectId, ref: "User" }, // User is table-name in mongoDB 6 | content: { type: String, trim: true }, 7 | chat: { type: mongoose.Schema.Types.ObjectId, ref: "Chat" }, //Chat -> table Name in mongoDB 8 | }, 9 | { 10 | timestamps: true, 11 | } 12 | ); 13 | 14 | const Message = mongoose.model("Message", messageModel); 15 | 16 | module.exports = Message; 17 | -------------------------------------------------------------------------------- /frontend/src/App.css: -------------------------------------------------------------------------------- 1 | @import url("https://fonts.googleapis.com/css2?family=Work+Sans:wght@300&display=swap"); 2 | 3 | .App { 4 | min-height: 100vh; 5 | display: flex; 6 | background-image: url("./background2.jpeg"); 7 | background-position: center; 8 | background-size: cover; 9 | } 10 | ::-webkit-scrollbar { 11 | width: 0px; 12 | } 13 | 14 | ::-webkit-scrollbar-thumb { 15 | background: rgba(136, 136, 136, 0.281); 16 | } 17 | 18 | /* Handle on hover */ 19 | ::-webkit-scrollbar-thumb:hover { 20 | background: #555; 21 | } 22 | -------------------------------------------------------------------------------- /frontend/src/Context/EncrDecr.js: -------------------------------------------------------------------------------- 1 | import * as CryptoJS from "crypto-js"; 2 | 3 | // const secretKey = process.env.REACT_APP_SECRET_KEY 4 | // ? process.env.REACT_APP_SECRET_KEY 5 | // : "12345"; 6 | 7 | export const encrypt = (plainText, secretKey) => { 8 | const cipherText = CryptoJS.AES.encrypt(plainText, secretKey).toString(); 9 | return cipherText; 10 | }; 11 | 12 | export const decrypt = (cipherText, secretKey) => { 13 | const bytes = CryptoJS.AES.decrypt(cipherText, secretKey); 14 | const plainText = bytes.toString(CryptoJS.enc.Utf8); 15 | return plainText; 16 | }; 17 | -------------------------------------------------------------------------------- /backend/EncrDecr.js: -------------------------------------------------------------------------------- 1 | const CryptoJS = require("crypto-js"); 2 | 3 | // const secretKey = process.env.REACT_APP_SECRET_KEY 4 | // ? process.env.REACT_APP_SECRET_KEY 5 | // : "12345"; 6 | 7 | const encrypt = (plainText, secretKey) => { 8 | const cipherText = CryptoJS.AES.encrypt(plainText, secretKey).toString(); 9 | return cipherText; 10 | }; 11 | 12 | const decrypt = (cipherText, secretKey) => { 13 | const bytes = CryptoJS.AES.decrypt(cipherText, secretKey); 14 | const plainText = bytes.toString(CryptoJS.enc.Utf8); 15 | return plainText; 16 | }; 17 | 18 | module.exports = { encrypt, decrypt }; 19 | -------------------------------------------------------------------------------- /backend/middleware/errorMiddleware.js: -------------------------------------------------------------------------------- 1 | const notFound = (req, res, next) => { 2 | // next => jump to next error , if exit error 3 | const error = new Error(`Not Found - ${req.originalUrl}`); 4 | res.status(404); 5 | next(error); 6 | }; 7 | 8 | const errorHandler = (err, req, res, next) => { 9 | // other error 10 | const statusCode = res.statusCode === 200 ? 500 : res.statusCode; 11 | res.status(statusCode); 12 | res.json({ 13 | message: err.message, 14 | stack: process.env.NODE_ENV === "production" ? null : err.stack, 15 | }); 16 | }; 17 | 18 | module.exports = { notFound, errorHandler }; 19 | -------------------------------------------------------------------------------- /frontend/public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "134914.png", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /frontend/src/components/UserAvatar/UserBadgeItem.js: -------------------------------------------------------------------------------- 1 | import { CloseIcon } from "@chakra-ui/icons"; 2 | import { Badge } from "@chakra-ui/layout"; 3 | 4 | const UserBadgeItem = ({ user, handleFunction }) => { 5 | return ( 6 | 18 | {user.name} 19 | {/* {admin === user._id && (Admin)} */} 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default UserBadgeItem; 26 | -------------------------------------------------------------------------------- /frontend/src/components/ChatLoading.js: -------------------------------------------------------------------------------- 1 | import { Stack } from "@chakra-ui/layout"; 2 | import { Skeleton } from "@chakra-ui/skeleton"; 3 | 4 | const ChatLoading = () => { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | ); 21 | }; 22 | 23 | export default ChatLoading; 24 | -------------------------------------------------------------------------------- /frontend/src/components/ChatBox.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { ChatState } from "../Context/ChatProvider"; 3 | import { Box } from "@chakra-ui/layout"; 4 | import SingleChat from "./SingleChat"; 5 | 6 | const ChatBox = ({ fetchAgain, setFetchAgain }) => { 7 | const { selectedChat } = ChatState(); 8 | 9 | return ( 10 | 20 | 21 | 22 | ); 23 | }; 24 | 25 | export default ChatBox; 26 | -------------------------------------------------------------------------------- /backend/routes/chatRoutes.js: -------------------------------------------------------------------------------- 1 | const express = require("express"); 2 | const { protect } = require("../middleware/authMiddleware"); 3 | // const { route } = require("./userRoutes"); 4 | const { 5 | accessChat, 6 | fetchChats, 7 | createGroupChat, 8 | renameGroup, 9 | addToGroup, 10 | removeFromGroup, 11 | } = require("../controllers/chatControllers"); 12 | 13 | const router = express.Router(); 14 | 15 | router.route("/").post(protect, accessChat); 16 | router.route("/").get(protect, fetchChats); 17 | router.route("/group").post(protect, createGroupChat); 18 | router.route("/rename").put(protect, renameGroup); 19 | router.route("/groupremove").put(protect, removeFromGroup); 20 | router.route("/groupadd").put(protect, addToGroup); 21 | 22 | module.exports = router; 23 | -------------------------------------------------------------------------------- /backend/models/chatModel.js: -------------------------------------------------------------------------------- 1 | //chat name 2 | //isGroupChat 3 | //users 4 | //latestMessage 5 | //groupAdmin 6 | 7 | const mongoose = require("mongoose"); 8 | 9 | const chatModel = mongoose.Schema( 10 | { 11 | chatName: { type: String, trim: true }, // either one to one chat or groupchat 12 | isGroupChat: { type: Boolean, default: false }, 13 | users: [ 14 | { 15 | type: mongoose.Schema.Types.ObjectId, 16 | ref: "User", 17 | }, 18 | ], 19 | lastestMessage: { 20 | type: mongoose.Schema.Types.ObjectId, 21 | ref: "Message", 22 | }, 23 | groupAdmin: { 24 | type: mongoose.Schema.Types.ObjectId, 25 | ref: "User", 26 | }, 27 | }, 28 | 29 | { 30 | timestamps: true, 31 | } 32 | ); 33 | 34 | const Chat = mongoose.model("Chat", chatModel); 35 | 36 | module.exports = Chat; 37 | -------------------------------------------------------------------------------- /frontend/src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | // import reportWebVitals from "./reportWebVitals"; 6 | import { ChakraProvider } from "@chakra-ui/react"; 7 | import ChatProvider from "./Context/ChatProvider"; 8 | import { BrowserRouter } from "react-router-dom"; 9 | 10 | ReactDOM.render( 11 | 12 | 13 | 14 | 15 | 16 | 17 | {/* must after chatprovider */} 18 | , 19 | document.getElementById("root") 20 | ); 21 | 22 | // If you want to start measuring performance in your app, pass a function 23 | // to log results (for example: reportWebVitals(console.log)) 24 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 25 | // reportWebVitals(); 26 | -------------------------------------------------------------------------------- /frontend/src/Pages/Chatpage.js: -------------------------------------------------------------------------------- 1 | import { Box } from "@chakra-ui/layout"; 2 | import { useState } from "react"; 3 | import Chatbox from "../components/ChatBox"; 4 | import MyChats from "../components/MyChats"; 5 | import SideDrawer from "../components/miscellaneous/SideDrawer"; 6 | import { ChatState } from "../Context/ChatProvider"; 7 | 8 | const Chatpage = () => { 9 | const [fetchAgain, setFetchAgain] = useState(false); 10 | const { user } = ChatState(); 11 | 12 | return ( 13 |
14 | {user && } 15 | 16 | {user && } 17 | {user && ( 18 | 19 | )} 20 | 21 |
22 | ); 23 | }; 24 | 25 | export default Chatpage; 26 | -------------------------------------------------------------------------------- /backend/middleware/authMiddleware.js: -------------------------------------------------------------------------------- 1 | const jwt = require("jsonwebtoken"); 2 | const User = require("../models/userModel.js"); 3 | const asyncHandler = require("express-async-handler"); 4 | 5 | const protect = asyncHandler(async (req, res, next) => { 6 | let token; 7 | 8 | if ( 9 | req.headers.authorization && 10 | req.headers.authorization.startsWith("Bearer") 11 | ) { 12 | try { 13 | token = req.headers.authorization.split(" ")[1]; 14 | 15 | //decodes token id 16 | const decoded = jwt.verify(token, process.env.JWT_SECRET); 17 | 18 | req.user = await User.findById(decoded.id).select("-password"); 19 | 20 | next(); 21 | } catch (error) { 22 | res.status(401); 23 | throw new Error("Not authorized, token failed"); 24 | } 25 | } 26 | 27 | if (!token) { 28 | res.status(401); 29 | throw new Error("Not authorized, no token"); 30 | } 31 | }); 32 | 33 | module.exports = { protect }; 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "jerry-chat", 3 | "version": "1.0.0", 4 | "description": "it is basic chat module.", 5 | "main": "server.js", 6 | "scripts": { 7 | "start": "nodemon backend/server.js", 8 | "server": "nodemon backend/server.js", 9 | "build": "npm install && npm install --prefix frontend && npm run build --prefix frontend" 10 | }, 11 | "keywords": [ 12 | "mern", 13 | "chat-app" 14 | ], 15 | "author": "Nitin Vadadoriya", 16 | "license": "ISC", 17 | "dependencies": { 18 | "@chakra-ui/react": "^2.8.2", 19 | "@emotion/react": "^11.11.3", 20 | "@emotion/styled": "^11.11.0", 21 | "@types/crypto-js": "^4.2.1", 22 | "bcryptjs": "^2.4.3", 23 | "crypto-js": "^4.2.0", 24 | "dotenv": "^9.0.2", 25 | "express": "^4.17.1", 26 | "express-async-handler": "^1.1.4", 27 | "framer-motion": "^10.16.16", 28 | "jsonwebtoken": "^8.5.1", 29 | "mongoose": "^5.12.9", 30 | "nodemon": "^3.0.2", 31 | "socket.io": "^4.7.3" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /frontend/src/components/UserAvatar/UserListItem.js: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@chakra-ui/avatar"; 2 | import { Box, Text } from "@chakra-ui/layout"; 3 | 4 | const UserListItem = ({ user, handleFunction }) => { 5 | return ( 6 | 23 | 30 | 31 | {user.name} 32 | 33 | Email : 34 | {user.email} 35 | 36 | 37 | 38 | ); 39 | }; 40 | 41 | export default UserListItem; 42 | -------------------------------------------------------------------------------- /backend/models/userModel.js: -------------------------------------------------------------------------------- 1 | const mongoose = require("mongoose"); 2 | const bcrypt = require("bcryptjs"); 3 | 4 | const userSchema = mongoose.Schema( 5 | { 6 | name: { type: String, required: true }, 7 | email: { type: String, required: true, unique: true }, 8 | password: { type: String, required: true }, 9 | pic: { 10 | type: String, 11 | default: 12 | "https://icon-library.com/images/anonymous-avatar-icon/anonymous-avatar-icon-25.jpg", 13 | }, 14 | }, 15 | 16 | { 17 | timestamps: true, 18 | } 19 | ); 20 | 21 | userSchema.methods.matchPassword = async function (enteredPassword) { 22 | return await bcrypt.compare(enteredPassword, this.password); 23 | }; 24 | 25 | userSchema.pre("save", async function (next) { 26 | if (!this.isModified) { 27 | next(); 28 | } 29 | 30 | const salt = await bcrypt.genSalt(10); 31 | this.password = await bcrypt.hash(this.password, salt); 32 | }); 33 | 34 | const User = mongoose.model("User", userSchema); 35 | 36 | module.exports = User; 37 | -------------------------------------------------------------------------------- /frontend/src/Context/ChatProvider.js: -------------------------------------------------------------------------------- 1 | import React, { createContext, useContext, useEffect, useState } from "react"; 2 | import { useHistory } from "react-router-dom"; 3 | 4 | const ChatContext = createContext(); 5 | 6 | const ChatProvider = ({ children }) => { 7 | const [selectedChat, setSelectedChat] = useState(); 8 | const [user, setUser] = useState(); 9 | const [notification, setNotification] = useState([]); 10 | const [chats, setChats] = useState([]); //current user all chats 11 | 12 | const history = useHistory(); 13 | 14 | useEffect(() => { 15 | const userInfo = JSON.parse(localStorage.getItem("userInfo")); 16 | setUser(userInfo); 17 | 18 | if (!userInfo) history.push("/"); 19 | // eslint-disable-next-line react-hooks/exhaustive-deps 20 | }, [history]); 21 | 22 | return ( 23 | 35 | {children} 36 | 37 | ); 38 | }; 39 | 40 | export const ChatState = () => { 41 | return useContext(ChatContext); 42 | }; 43 | 44 | export default ChatProvider; 45 | -------------------------------------------------------------------------------- /frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend", 3 | "version": "0.1.0", 4 | "private": true, 5 | "proxy": "http://127.0.0.1:5000", 6 | "dependencies": { 7 | "@chakra-ui/icons": "^1.1.7", 8 | "@chakra-ui/react": "^1.8.9", 9 | "@testing-library/jest-dom": "^5.17.0", 10 | "@testing-library/react": "^13.4.0", 11 | "@testing-library/user-event": "^13.5.0", 12 | "@types/crypto-js": "^4.2.1", 13 | "axios": "^0.21.4", 14 | "crypto-js": "^4.2.0", 15 | "framer-motion": "^4.1.17", 16 | "react": "^17.0.2", 17 | "react-chips": "^0.8.0", 18 | "react-dom": "^17.0.2", 19 | "react-lottie": "^1.2.4", 20 | "react-notification-badge": "^1.5.1", 21 | "react-router-dom": "^5.2.0", 22 | "react-scripts": "^4.0.3", 23 | "react-scrollable-feed": "^1.3.2", 24 | "socket.io-client": "^4.7.3", 25 | "web-vitals": "^1.1.2" 26 | }, 27 | "scripts": { 28 | "start": "react-scripts --openssl-legacy-provider start", 29 | "build": "react-scripts --openssl-legacy-provider build", 30 | "test": "react-scripts test", 31 | "eject": "react-scripts eject" 32 | }, 33 | "eslintConfig": { 34 | "extends": [ 35 | "react-app", 36 | "react-app/jest" 37 | ] 38 | }, 39 | "browserslist": { 40 | "production": [ 41 | ">0.2%", 42 | "not dead", 43 | "not op_mini all" 44 | ], 45 | "development": [ 46 | "last 1 chrome version", 47 | "last 1 firefox version", 48 | "last 1 safari version" 49 | ] 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /frontend/src/config/ChatLogics.js: -------------------------------------------------------------------------------- 1 | export const isSameSenderMargin = (messages, m, i, userId) => { 2 | if ( 3 | i < messages.length - 1 && 4 | messages[i + 1].sender._id === m.sender._id && 5 | messages[i].sender._id !== userId 6 | ) 7 | return 33; 8 | else if ( 9 | (i < messages.length - 1 && 10 | messages[i + 1].sender._id !== m.sender._id && 11 | messages[i].sender._id !== userId) || 12 | (i === messages.length - 1 && messages[i].sender._id !== userId) 13 | ) 14 | return 0; 15 | else return "auto"; 16 | }; 17 | 18 | export const isSameSender = (messages, m, i, userId) => { 19 | return ( 20 | i < messages.length - 1 && 21 | (messages[i + 1].sender._id !== m.sender._id || 22 | messages[i + 1].sender._id === undefined) && 23 | messages[i].sender._id !== userId 24 | ); 25 | }; 26 | 27 | export const isLastMessage = (messages, i, userId) => { 28 | return ( 29 | i === messages.length - 1 && 30 | messages[messages.length - 1].sender._id !== userId && 31 | messages[messages.length - 1].sender._id 32 | ); 33 | }; 34 | 35 | export const isSameUser = (messages, m, i) => { 36 | return i > 0 && messages[i - 1].sender._id === m.sender._id; // for make margin in between two chat 37 | }; 38 | 39 | export const getSender = (loggedUser, users) => { 40 | return users[0]?._id === loggedUser?._id ? users[1].name : users[0].name; 41 | }; 42 | 43 | export const getSenderFull = (loggedUser, users) => { 44 | return users[0]._id === loggedUser._id ? users[1] : users[0]; 45 | }; 46 | -------------------------------------------------------------------------------- /frontend/src/Pages/Homepage.js: -------------------------------------------------------------------------------- 1 | import { 2 | Box, 3 | Container, 4 | Tab, 5 | TabList, 6 | TabPanel, 7 | TabPanels, 8 | Tabs, 9 | Text, 10 | } from "@chakra-ui/react"; 11 | import { useEffect } from "react"; 12 | import { useHistory } from "react-router"; 13 | import Login from "../components/Authentication/Login"; 14 | import Signup from "../components/Authentication/Signup"; 15 | 16 | function Homepage() { 17 | const history = useHistory(); 18 | 19 | useEffect(() => { 20 | const user = JSON.parse(localStorage.getItem("userInfo")); 21 | 22 | if (user) history.push("/chats"); 23 | }, [history]); 24 | 25 | return ( 26 | 27 | 37 | 38 | JerryChat 39 | 40 | 41 | NitinCode 42 | 43 | 44 | 45 | 46 | 47 | Login 48 | Sign Up 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | ); 62 | } 63 | 64 | export default Homepage; 65 | -------------------------------------------------------------------------------- /frontend/src/components/miscellaneous/ProfileModal.js: -------------------------------------------------------------------------------- 1 | import { ViewIcon } from "@chakra-ui/icons"; 2 | import { 3 | Modal, 4 | ModalOverlay, 5 | ModalContent, 6 | ModalHeader, 7 | ModalFooter, 8 | ModalBody, 9 | ModalCloseButton, 10 | Button, 11 | useDisclosure, 12 | IconButton, 13 | Text, 14 | Image, 15 | } from "@chakra-ui/react"; 16 | 17 | const ProfileModal = ({ user, children }) => { 18 | const { isOpen, onOpen, onClose } = useDisclosure(); 19 | 20 | return ( 21 | <> 22 | {children ? ( 23 | {children} 24 | ) : ( 25 | } onClick={onOpen} /> 26 | )} 27 | 28 | 29 | 30 | 31 | 37 | {user.name} 38 | 39 | 40 | 46 | {user.name} 52 | 56 | Email: {user.email} 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | }; 67 | 68 | export default ProfileModal; 69 | -------------------------------------------------------------------------------- /frontend/src/components/ScrollableChat.js: -------------------------------------------------------------------------------- 1 | import { Avatar } from "@chakra-ui/avatar"; 2 | import { Tooltip } from "@chakra-ui/tooltip"; 3 | import ScrollableFeed from "react-scrollable-feed"; 4 | import { decrypt } from "../Context/EncrDecr"; 5 | 6 | import { 7 | isLastMessage, 8 | isSameSender, 9 | isSameSenderMargin, 10 | isSameUser, 11 | } from "../config/ChatLogics"; 12 | import { ChatState } from "../Context/ChatProvider"; 13 | 14 | const ScrollableChat = ({ messages }) => { 15 | const { user } = ChatState(); 16 | 17 | return ( 18 | 19 | {messages && 20 | messages.map((m, i) => ( 21 |
22 | {(isSameSender(messages, m, i, user._id) || 23 | isLastMessage(messages, i, user._id)) && ( 24 | 25 | 33 | 34 | )} 35 | 47 | {/* {m.chat._id} */} 48 | {decrypt(m.content, m.chat._id)} 49 | 50 |
51 | ))} 52 |
53 | ); 54 | }; 55 | 56 | export default ScrollableChat; 57 | -------------------------------------------------------------------------------- /frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 25 | 34 | JerryChat 35 | 36 | 37 | 38 |
39 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /backend/controllers/userControllers.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = require("express-async-handler"); 2 | const User = require("../models/userModel"); 3 | const generateToken = require("../config/generateToken"); 4 | const { decrypt } = require("../EncrDecr"); 5 | 6 | // for sign-up 7 | const registerUser = asyncHandler(async (req, res) => { 8 | // asyncHandler handle error 9 | const { name, email, password, pic } = req.body; 10 | 11 | if (!name || !email || !password) { 12 | // atleast one value undefien 13 | res.status(400); 14 | throw new Error("Please Enter all the Feilds"); 15 | } 16 | 17 | const userExists = await User.findOne({ email }); 18 | 19 | if (userExists) { 20 | res.status(400); 21 | throw new Error("User already exists"); 22 | } 23 | 24 | const user = await User.create({ 25 | // make entry in databaase for new user-registration 26 | name, 27 | email, 28 | password, 29 | pic, 30 | }); // if succesfully then return all value in user variable 31 | 32 | if (user) { 33 | res.status(201).json({ 34 | _id: user._id, 35 | name: user.name, 36 | email: user.email, 37 | isAdmin: user.isAdmin, 38 | pic: user.pic, 39 | token: generateToken(user._id), // send jwt token to user 40 | }); 41 | } else { 42 | res.status(400); 43 | throw new Error("User not found"); 44 | } 45 | }); 46 | 47 | //for login 48 | const authUser = asyncHandler(async (req, res) => { 49 | const { email, password } = req.body; 50 | 51 | const user = await User.findOne({ email }); 52 | 53 | if (user && (await user.matchPassword(password))) { 54 | res.json({ 55 | _id: user._id, 56 | name: user.name, 57 | email: user.email, 58 | isAdmin: user.isAdmin, 59 | pic: user.pic, 60 | token: generateToken(user._id), 61 | }); 62 | } else { 63 | res.status(401); 64 | throw new Error("Invalid Email or Password"); 65 | } 66 | }); 67 | 68 | // /api/user?search=nitin 69 | const allUsers = asyncHandler(async (req, res) => { 70 | const keyword = req.query.search 71 | ? { 72 | $or: [ 73 | { name: { $regex: req.query.search, $options: "i" } }, 74 | { email: { $regex: req.query.search, $options: "i" } }, 75 | ], 76 | } 77 | : {}; 78 | 79 | const users = await User.find(keyword).find({ _id: { $ne: req.user._id } }); 80 | res.send(users); 81 | }); 82 | 83 | module.exports = { registerUser, authUser, allUsers }; 84 | -------------------------------------------------------------------------------- /backend/controllers/messageControllers.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = require("express-async-handler"); 2 | const Message = require("../models/messageModel"); 3 | const User = require("../models/userModel"); 4 | const Chat = require("../models/chatModel"); 5 | 6 | // //@description Get all Messages 7 | // //@route GET /api/Message/:chatId 8 | // //@access Protected 9 | // const allMessages = asyncHandler(async (req, res) => { 10 | // try { 11 | // const messages = await Message.find({ chat: req.params.chatId }) 12 | // .populate("sender", "name pic email") 13 | // .populate("chat"); 14 | // res.json(messages); 15 | // } catch (error) { 16 | // res.status(400); 17 | // throw new Error(error.message); 18 | // } 19 | // }); 20 | 21 | //@description Create New Message 22 | //@route POST /api/Message/ 23 | //@access Protected 24 | const sendMessage = asyncHandler(async (req, res) => { 25 | const { content, chatId } = req.body; 26 | 27 | if (!content || !chatId) { 28 | console.log("Invalid data passed into request"); 29 | return res.sendStatus(400); 30 | } 31 | 32 | var newMessage = { 33 | sender: req.user._id, 34 | content: content, 35 | chat: chatId, 36 | }; 37 | 38 | try { 39 | var message = await Message.create(newMessage); 40 | 41 | message = await message.populate("sender", "name pic").execPopulate(); 42 | message = await message.populate("chat").execPopulate(); 43 | message = await User.populate(message, { 44 | path: "chat.users", 45 | select: "name pic email", 46 | }); 47 | 48 | await Chat.findByIdAndUpdate(req.body.chatId, { latestMessage: message }); 49 | // await Chat.findOneAndUpdate( 50 | // { chatId: req.body.chatId }, 51 | // { latestMessage: message }, 52 | // { includeResultMetadata: true } 53 | // ); 54 | 55 | res.json(message); 56 | } catch (error) { 57 | res.status(400); 58 | throw new Error(error.message); 59 | } 60 | }); 61 | 62 | //@description Get all Messages 63 | //@route GET /api/Message/:chatId 64 | //@access Protected 65 | const allMessages = asyncHandler(async (req, res) => { 66 | try { 67 | const messages = await Message.find({ chat: req.params.chatId }) 68 | .populate("sender", "name pic email") 69 | .populate("chat"); 70 | res.json(messages); 71 | } catch (error) { 72 | res.status(400); 73 | throw new Error(error.message); 74 | } 75 | }); 76 | 77 | module.exports = { sendMessage, allMessages }; 78 | -------------------------------------------------------------------------------- /backend/data/data.js: -------------------------------------------------------------------------------- 1 | const chats = [ 2 | { 3 | isGroupChat: false, 4 | users: [ 5 | { 6 | name: "John Doe", 7 | email: "john@example.com", 8 | }, 9 | { 10 | name: "Piyush", 11 | email: "piyush@example.com", 12 | }, 13 | ], 14 | _id: "617a077e18c25468bc7c4dd4", 15 | chatName: "John Doe", 16 | }, 17 | { 18 | isGroupChat: false, 19 | users: [ 20 | { 21 | name: "Guest User", 22 | email: "guest@example.com", 23 | }, 24 | { 25 | name: "Piyush", 26 | email: "piyush@example.com", 27 | }, 28 | ], 29 | _id: "617a077e18c25468b27c4dd4", 30 | chatName: "Guest User", 31 | }, 32 | { 33 | isGroupChat: false, 34 | users: [ 35 | { 36 | name: "Anthony", 37 | email: "anthony@example.com", 38 | }, 39 | { 40 | name: "Piyush", 41 | email: "piyush@example.com", 42 | }, 43 | ], 44 | _id: "617a077e18c2d468bc7c4dd4", 45 | chatName: "Anthony", 46 | }, 47 | { 48 | isGroupChat: true, 49 | users: [ 50 | { 51 | name: "John Doe", 52 | email: "jon@example.com", 53 | }, 54 | { 55 | name: "Piyush", 56 | email: "piyush@example.com", 57 | }, 58 | { 59 | name: "Guest User", 60 | email: "guest@example.com", 61 | }, 62 | ], 63 | _id: "617a518c4081150716472c78", 64 | chatName: "Friends", 65 | groupAdmin: { 66 | name: "Guest User", 67 | email: "guest@example.com", 68 | }, 69 | }, 70 | { 71 | isGroupChat: false, 72 | users: [ 73 | { 74 | name: "Jane Doe", 75 | email: "jane@example.com", 76 | }, 77 | { 78 | name: "Piyush", 79 | email: "piyush@example.com", 80 | }, 81 | ], 82 | _id: "617a077e18c25468bc7cfdd4", 83 | chatName: "Jane Doe", 84 | }, 85 | { 86 | isGroupChat: true, 87 | users: [ 88 | { 89 | name: "John Doe", 90 | email: "jon@example.com", 91 | }, 92 | { 93 | name: "Piyush", 94 | email: "piyush@example.com", 95 | }, 96 | { 97 | name: "Guest User", 98 | email: "guest@example.com", 99 | }, 100 | ], 101 | _id: "617a518c4081150016472c78", 102 | chatName: "Chill Zone", 103 | groupAdmin: { 104 | name: "Guest User", 105 | email: "guest@example.com", 106 | }, 107 | }, 108 | ]; 109 | 110 | module.exports = { chats }; // export the chats array from a module in Node.js -------------------------------------------------------------------------------- /backend/server.js: -------------------------------------------------------------------------------- 1 | //import library 2 | const express = require("express"); 3 | const { chats } = require("./data/data"); // inport chats with de-structuring. 4 | const dotenv = require("dotenv"); 5 | const connectDB = require("./config/db"); 6 | const userRoutes = require("./routes/userRoutes"); 7 | const chatRoutes = require("./routes/chatRoutes"); 8 | const messageRoutes = require("./routes/messageRoutes"); 9 | const { notFound, errorHandler } = require("./middleware/errorMiddleware"); 10 | const { Socket } = require("socket.io"); 11 | const path = require("path"); 12 | 13 | dotenv.config(); 14 | connectDB(); // must call after dotenv.config() 15 | const app = express(); // create object/instance of express-class 16 | 17 | app.use(express.json()); // to accept json data 18 | 19 | app.use("/api/user", userRoutes); 20 | app.use("/api/chat", chatRoutes); 21 | app.use("/api/message", messageRoutes); 22 | 23 | // --------------------------deployment------------------------------ 24 | 25 | const __dirname1 = path.resolve(); 26 | 27 | if (process.env.NODE_ENV === "production") { 28 | app.use(express.static(path.join(__dirname1, "/frontend/build"))); 29 | 30 | app.get("*", (req, res) => 31 | res.sendFile(path.resolve(__dirname1, "frontend", "build", "index.html")) 32 | ); 33 | } else { 34 | app.get("/", (req, res) => { 35 | res.send("API is running.."); 36 | }); 37 | } 38 | 39 | // --------------------------deployment------------------------------ 40 | 41 | // Error Handling middlewares 42 | app.use(notFound); 43 | app.use(errorHandler); 44 | 45 | const PORT = process.env.PORT || 5000; 46 | const server = app.listen(PORT, () => { 47 | console.log(`Server is running on port ${PORT}`); 48 | }); 49 | 50 | const io = require("socket.io")(server, { 51 | pingTimeout: 60000, 52 | cors: { 53 | origin: "https://jerrychat.onrender.com/", // frontend connection 54 | // origin: "http://127.0.0.1:3000", // localHost 55 | }, 56 | }); 57 | 58 | io.on("connection", (socket) => { 59 | console.log("Connected to socket.io"); 60 | 61 | socket.on("setup", (userData) => { 62 | // every online user joined 63 | socket.join(userData._id); // user create they own vertual-room based on id 64 | socket.emit("connected"); 65 | }); 66 | 67 | socket.on("join chat", (room) => { 68 | // chat-users only join vertiual-room 69 | socket.join(room); // room is chat-id, vertual-room was created based on chat-id 70 | }); 71 | socket.on("typing", (room) => socket.in(room).emit("typing")); 72 | socket.on("stop typing", (room) => socket.in(room).emit("stop typing")); 73 | 74 | socket.on("new message", (newMessageRecieved) => { 75 | var chat = newMessageRecieved.chat; 76 | 77 | if (!chat.users) return console.log("chat.users not defined"); 78 | 79 | chat.users.forEach((user) => { 80 | if (user._id == newMessageRecieved.sender._id) return; 81 | 82 | socket.in(user._id).emit("message recieved", newMessageRecieved); 83 | }); 84 | }); 85 | 86 | socket.off("setup", () => { 87 | console.log("USER DISCONNECTED"); 88 | socket.leave(userData._id); 89 | }); 90 | }); 91 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # JerryChat 3 | 4 | JerryChat is a Full Stack Chatting App. Uses Socket.io for real time communication and stores user details(sensetive) in encrypted format in Mongo DB Database. 5 | 6 | 7 | ## Technolgoy Stack 8 | 9 | **Client:** React JS. 10 | 11 | **Server:** Node JS, Express JS. 12 | 13 | **Database:** Mongo DB. 14 | 15 | ## Demo 16 | 17 | https://jerrychat.onrender.com/ 18 | 19 | 20 | 21 | 22 | ## Screenshots 23 | 24 | ![ScreenShot](https://raw.github.com/piyush-eon/mern-chat-app/master/screenshots/mainscreen.PNG) 25 | 26 | 27 | 28 | 29 | 30 | ## Run Locally 31 | 32 | Clone the project 33 | 34 | ```bash 35 | git clone https://github.com/NitinVadadoriyaa/JerryChat 36 | ``` 37 | 38 | Go to the project directory 39 | 40 | ```bash 41 | cd ChatApp 42 | ``` 43 | 44 | Install dependencies 45 | 46 | ```bash 47 | npm install 48 | ``` 49 | ```bash 50 | cd frontend/ 51 | npm install 52 | ``` 53 | 54 | Start the server 55 | 56 | ```bash 57 | npm run start 58 | ``` 59 | Start the client 60 | 61 | ```bash 62 | //open new terminal 63 | cd frontend 64 | npm run start 65 | ``` 66 | 67 | 68 | ## Environment Variables 69 | 70 | To run this project, you will need to add the following environment variables to your .env file 71 | 72 | `PORT=5000` 73 | 74 | `MONGO_URI = 'Your mongodb api'` 75 | 76 | 77 | `JWT_SECRET = "your secret name"` 78 | 79 | 80 | `NODE_ENV = production` 81 | 82 | `REACT_APP_SECRET_KEY = 'your number(atleast 10 digit)'` 83 | 84 | ## Note 85 | ``Before runing app make some change. 86 | 87 | 1. backend / server.js 88 | REPLACE : origin: "https://jerrychat.onrender.com/" To origin: "http://localhost:3000" 89 | 90 | 2. backend / server.js 91 | REMOVE THIS CODE 92 | 93 | const __dirname1 = path.resolve(); 94 | 95 | if (process.env.NODE_ENV === "production") { 96 | app.use(express.static(path.join(__dirname1, "/frontend/build"))); 97 | 98 | app.get("*", (req, res) => 99 | res.sendFile(path.resolve(__dirname1, "frontend", "build", "index.html")) 100 | ); 101 | } else { 102 | app.get("/", (req, res) => { 103 | res.send("API is running.."); 104 | }); 105 | } 106 | 107 | 3. /frontend/src/components/SingleChat.js 108 | REPLACE: ENDPOINT = "https://jerrychat.onrender.com/" TO ENDPOINT = "http://localhost:5000" 109 | ## Features 110 | 111 | - Authenticaton 112 | ![ScreenShot](https://raw.github.com/piyush-eon/mern-chat-app/master/screenshots/login.PNG) 113 | 114 | - Real Time Chatting with Typing indicators 115 | ![ScreenShot](https://raw.github.com/piyush-eon/mern-chat-app/master/screenshots/real-time.PNG) 116 | 117 | - One to One chat 118 | ![ScreenShot](https://raw.github.com/piyush-eon/mern-chat-app/master/screenshots/mainscreen.PNG) 119 | 120 | - Search Users 121 | ![ScreenShot](https://raw.github.com/piyush-eon/mern-chat-app/master/screenshots/search.PNG) 122 | 123 | - Create Group Chats 124 | ![ScreenShot](https://raw.github.com/NitinVadadoriyaa/JerryChat/master/screenshorts/newgrp.PNG) 125 | 126 | - Notifications 127 | ![ScreenShot](https://raw.github.com/NitinVadadoriyaa/JerryChat/master/screenshorts/group_notif.PNG) 128 | 129 | - Add or Remove users from group 130 | ![ScreenShot](https://raw.github.com/NitinVadadoriyaa/JerryChat/master/screenshorts/addrem.PNG) 131 | 132 | - View Other user Profile 133 | ![ScreenShot](https://raw.github.com/piyush-eon/mern-chat-app/master/screenshots/profile.PNG) 134 | 135 | 136 | 137 | 138 | ## Made By 139 | NitinVadadoriya 140 | -------------------------------------------------------------------------------- /frontend/README.md: -------------------------------------------------------------------------------- 1 | # Getting Started with Create React App 2 | 3 | This project was bootstrapped with [Create React App](https://github.com/facebook/create-react-app). 4 | 5 | ## Available Scripts 6 | 7 | In the project directory, you can run: 8 | 9 | ### `npm start` 10 | 11 | Runs the app in the development mode.\ 12 | Open [http://localhost:3000](http://localhost:3000) to view it in your browser. 13 | 14 | The page will reload when you make changes.\ 15 | You may also see any lint errors in the console. 16 | 17 | ### `npm test` 18 | 19 | Launches the test runner in the interactive watch mode.\ 20 | See the section about [running tests](https://facebook.github.io/create-react-app/docs/running-tests) for more information. 21 | 22 | ### `npm run build` 23 | 24 | Builds the app for production to the `build` folder.\ 25 | It correctly bundles React in production mode and optimizes the build for the best performance. 26 | 27 | The build is minified and the filenames include the hashes.\ 28 | Your app is ready to be deployed! 29 | 30 | See the section about [deployment](https://facebook.github.io/create-react-app/docs/deployment) for more information. 31 | 32 | ### `npm run eject` 33 | 34 | **Note: this is a one-way operation. Once you `eject`, you can't go back!** 35 | 36 | If you aren't satisfied with the build tool and configuration choices, you can `eject` at any time. This command will remove the single build dependency from your project. 37 | 38 | Instead, it will copy all the configuration files and the transitive dependencies (webpack, Babel, ESLint, etc) right into your project so you have full control over them. All of the commands except `eject` will still work, but they will point to the copied scripts so you can tweak them. At this point you're on your own. 39 | 40 | You don't have to ever use `eject`. The curated feature set is suitable for small and middle deployments, and you shouldn't feel obligated to use this feature. However we understand that this tool wouldn't be useful if you couldn't customize it when you are ready for it. 41 | 42 | ## Learn More 43 | 44 | You can learn more in the [Create React App documentation](https://facebook.github.io/create-react-app/docs/getting-started). 45 | 46 | To learn React, check out the [React documentation](https://reactjs.org/). 47 | 48 | ### Code Splitting 49 | 50 | This section has moved here: [https://facebook.github.io/create-react-app/docs/code-splitting](https://facebook.github.io/create-react-app/docs/code-splitting) 51 | 52 | ### Analyzing the Bundle Size 53 | 54 | This section has moved here: [https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size](https://facebook.github.io/create-react-app/docs/analyzing-the-bundle-size) 55 | 56 | ### Making a Progressive Web App 57 | 58 | This section has moved here: [https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app](https://facebook.github.io/create-react-app/docs/making-a-progressive-web-app) 59 | 60 | ### Advanced Configuration 61 | 62 | This section has moved here: [https://facebook.github.io/create-react-app/docs/advanced-configuration](https://facebook.github.io/create-react-app/docs/advanced-configuration) 63 | 64 | ### Deployment 65 | 66 | This section has moved here: [https://facebook.github.io/create-react-app/docs/deployment](https://facebook.github.io/create-react-app/docs/deployment) 67 | 68 | ### `npm run build` fails to minify 69 | 70 | This section has moved here: [https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify](https://facebook.github.io/create-react-app/docs/troubleshooting#npm-run-build-fails-to-minify) 71 | -------------------------------------------------------------------------------- /frontend/src/components/Authentication/Login.js: -------------------------------------------------------------------------------- 1 | import { Button } from "@chakra-ui/button"; 2 | import { FormControl, FormLabel } from "@chakra-ui/form-control"; 3 | import { Input, InputGroup, InputRightElement } from "@chakra-ui/input"; 4 | import { VStack } from "@chakra-ui/layout"; 5 | import { useState } from "react"; 6 | import axios from "axios"; 7 | import { useToast } from "@chakra-ui/react"; 8 | import { useHistory } from "react-router-dom"; 9 | import { ChatState } from "../../Context/ChatProvider"; 10 | import { encrypt } from "../../Context/EncrDecr"; 11 | 12 | const Login = () => { 13 | const [show, setShow] = useState(false); 14 | const handleClick = () => setShow(!show); 15 | const toast = useToast(); 16 | const [email, setEmail] = useState(); 17 | const [password, setPassword] = useState(); 18 | const [loading, setLoading] = useState(false); 19 | 20 | const history = useHistory(); 21 | const { setUser } = ChatState(); 22 | 23 | const submitHandler = async () => { 24 | setLoading(true); 25 | if (!email || !password) { 26 | toast({ 27 | title: "Please Fill all the Feilds", 28 | status: "warning", 29 | duration: 5000, 30 | isClosable: true, 31 | position: "bottom", 32 | }); 33 | setLoading(false); 34 | return; 35 | } 36 | 37 | try { 38 | const config = { 39 | headers: { 40 | "Content-type": "application/json", 41 | }, 42 | }; 43 | // const newEmail = encrypt(email); 44 | // const newPassword = encrypt(password); 45 | 46 | console.log("E " + email + " P " + password); 47 | 48 | const { data } = await axios.post( 49 | "/api/user/login", 50 | { email, password }, 51 | config 52 | ); 53 | 54 | toast({ 55 | title: "Login Successful", 56 | status: "success", 57 | duration: 5000, 58 | isClosable: true, 59 | position: "bottom", 60 | }); 61 | setUser(data); 62 | localStorage.setItem("userInfo", JSON.stringify(data)); 63 | setLoading(false); 64 | history.push("/chats"); 65 | } catch (error) { 66 | toast({ 67 | title: "Error Occured!", 68 | description: error.response.data.message, 69 | status: "error", 70 | duration: 5000, 71 | isClosable: true, 72 | position: "bottom", 73 | }); 74 | setLoading(false); 75 | } 76 | }; 77 | 78 | return ( 79 | 80 | 81 | Email Address 82 | setEmail(e.target.value)} 87 | /> 88 | 89 | 90 | Password 91 | 92 | setPassword(e.target.value)} 95 | type={show ? "text" : "password"} 96 | placeholder="Enter password" 97 | /> 98 | 99 | 102 | 103 | 104 | 105 | 114 | 125 | 126 | ); 127 | }; 128 | 129 | export default Login; 130 | -------------------------------------------------------------------------------- /frontend/src/components/MyChats.js: -------------------------------------------------------------------------------- 1 | import { AddIcon } from "@chakra-ui/icons"; 2 | import { Box, Stack, Text } from "@chakra-ui/layout"; 3 | import { useToast } from "@chakra-ui/toast"; 4 | import axios from "axios"; 5 | import { useEffect, useState } from "react"; 6 | // import { getSender } from "../config/ChatLogics"; 7 | // import ChatLoading from "./ChatLoading"; 8 | // import GroupChatModal from "./miscellaneous/GroupChatModal"; 9 | import { Button } from "@chakra-ui/react"; 10 | import { ChatState } from "../Context/ChatProvider"; 11 | import { getSender } from "../config/ChatLogics"; 12 | import ChatLoading from "./ChatLoading"; 13 | import GroupChatModal from "./miscellaneous/GroupChatModal"; 14 | 15 | const MyChats = ({ fetchAgain }) => { 16 | const [loggedUser, setLoggedUser] = useState(); 17 | 18 | const { selectedChat, setSelectedChat, user, chats, setChats } = ChatState(); 19 | 20 | const toast = useToast(); 21 | 22 | const fetchChats = async () => { 23 | try { 24 | const config = { 25 | headers: { 26 | Authorization: `Bearer ${user.token}`, 27 | }, 28 | }; 29 | 30 | const { data } = await axios.get("/api/chat", config); 31 | setChats(data); 32 | } catch (error) { 33 | toast({ 34 | title: "Error Occured!", 35 | description: "Failed to Load the chats", 36 | status: "error", 37 | duration: 5000, 38 | isClosable: true, 39 | position: "bottom-left", 40 | }); 41 | } 42 | }; 43 | 44 | useEffect(() => { 45 | setLoggedUser(JSON.parse(localStorage.getItem("userInfo"))); 46 | fetchChats(); 47 | // eslint-disable-next-line 48 | }, [fetchAgain]); 49 | 50 | return ( 51 | 61 | 71 | My Chats 72 | 73 | 80 | 81 | 82 | 92 | {chats ? ( 93 | 94 | {chats.map((chat) => ( 95 | setSelectedChat(chat)} 97 | cursor="pointer" 98 | bg={selectedChat === chat ? "#38B2AC" : "#E8E8E8"} 99 | color={selectedChat === chat ? "white" : "black"} 100 | px={3} 101 | py={2} 102 | borderRadius="lg" 103 | key={chat._id} 104 | > 105 | 106 | {!chat.isGroupChat 107 | ? getSender(loggedUser, chat.users) 108 | : chat.chatName} 109 | 110 | {chat.latestMessage && ( 111 | 112 | {chat.latestMessage.sender.name} : 113 | {chat.latestMessage.content.length > 50 114 | ? chat.latestMessage.content.substring(0, 51) + "..." 115 | : chat.latestMessage.content} 116 | 117 | )} 118 | 119 | ))} 120 | 121 | ) : ( 122 | 123 | )} 124 | 125 | 126 | ); 127 | }; 128 | 129 | export default MyChats; 130 | -------------------------------------------------------------------------------- /backend/controllers/chatControllers.js: -------------------------------------------------------------------------------- 1 | const asyncHandler = require("express-async-handler"); 2 | const Chat = require("../models/chatModel"); 3 | const User = require("../models/userModel"); 4 | 5 | //@description Create or fetch One to One Chat 6 | //@route POST /api/chat/ 7 | //@access Protected 8 | const accessChat = asyncHandler(async (req, res) => { 9 | const { userId } = req.body; 10 | 11 | if (!userId) { 12 | console.log("UserId param not sent with request"); 13 | return res.sendStatus(400); 14 | } 15 | 16 | var isChat = await Chat.find({ 17 | isGroupChat: false, // since one to one chat 18 | $and: [ 19 | { users: { $elemMatch: { $eq: req.user._id } } }, 20 | { users: { $elemMatch: { $eq: userId } } }, 21 | ], 22 | }) 23 | .populate("users", "-password") // eleminate password 24 | .populate("latestMessage"); // eliminate latestMessage 25 | 26 | isChat = await User.populate(isChat, { 27 | path: "latestMessage.sender", 28 | select: "name pic email", 29 | }); 30 | 31 | if (isChat.length > 0) { 32 | res.send(isChat[0]); 33 | } else { 34 | var chatData = { 35 | chatName: "sender", 36 | isGroupChat: false, 37 | users: [req.user._id, userId], 38 | }; 39 | 40 | try { 41 | const createdChat = await Chat.create(chatData); 42 | const FullChat = await Chat.findOne({ _id: createdChat._id }).populate( 43 | "users", 44 | "-password" 45 | ); 46 | res.status(200).json(FullChat); 47 | } catch (error) { 48 | res.status(400); 49 | throw new Error(error.message); 50 | } 51 | } 52 | }); 53 | 54 | //@description Fetch all chats for a user 55 | //@route GET /api/chat/ 56 | //@access Protected 57 | const fetchChats = asyncHandler(async (req, res) => { 58 | try { 59 | Chat.find({ users: { $elemMatch: { $eq: req.user._id } } }) 60 | .populate("users", "-password") 61 | .populate("groupAdmin", "-password") 62 | .populate("latestMessage") 63 | .sort({ updatedAt: -1 }) 64 | .then(async (results) => { 65 | results = await User.populate(results, { 66 | path: "latestMessage.sender", 67 | select: "name pic email", 68 | }); 69 | res.status(200).send(results); 70 | }); 71 | } catch (error) { 72 | res.status(400); 73 | throw new Error(error.message); 74 | } 75 | }); 76 | 77 | //@description Create New Group Chat 78 | //@route POST /api/chat/group 79 | //@access Protected 80 | const createGroupChat = asyncHandler(async (req, res) => { 81 | if (!req.body.users || !req.body.name) { 82 | return res.status(400).send({ message: "Please Fill all the feilds" }); 83 | } 84 | 85 | var users = JSON.parse(req.body.users); 86 | 87 | if (users.length < 2) { 88 | return res 89 | .status(400) 90 | .send("More than 2 users are required to form a group chat"); 91 | } 92 | 93 | users.push(req.user); // group contain current user also (group admin) 94 | 95 | try { 96 | const groupChat = await Chat.create({ 97 | chatName: req.body.name, 98 | users: users, 99 | isGroupChat: true, 100 | groupAdmin: req.user, 101 | }); 102 | 103 | const fullGroupChat = await Chat.findOne({ _id: groupChat._id }) 104 | .populate("users", "-password") 105 | .populate("groupAdmin", "-password"); 106 | 107 | res.status(200).json(fullGroupChat); 108 | } catch (error) { 109 | res.status(400); 110 | throw new Error(error.message); 111 | } 112 | }); 113 | 114 | // @desc Rename Group 115 | // @route PUT /api/chat/rename 116 | // @access Protected 117 | const renameGroup = asyncHandler(async (req, res) => { 118 | const { chatId, chatName } = req.body; // chatName == new group name 119 | 120 | const updatedChat = await Chat.findByIdAndUpdate( 121 | chatId, 122 | { 123 | chatName: chatName, 124 | }, 125 | { 126 | new: true, // send updated data 127 | } 128 | ) 129 | .populate("users", "-password") 130 | .populate("groupAdmin", "-password"); 131 | 132 | if (!updatedChat) { 133 | res.status(404); 134 | throw new Error("Chat Not Found"); 135 | } else { 136 | res.json(updatedChat); 137 | } 138 | }); 139 | 140 | // @desc Remove user from Group 141 | // @route PUT /api/chat/groupremove 142 | // @access Protected 143 | const removeFromGroup = asyncHandler(async (req, res) => { 144 | const { chatId, userId } = req.body; 145 | 146 | // check if the requester is admin 147 | 148 | const removed = await Chat.findByIdAndUpdate( 149 | chatId, 150 | { 151 | $pull: { users: userId }, // at that time one user can removed 152 | }, 153 | { 154 | new: true, 155 | } 156 | ) 157 | .populate("users", "-password") 158 | .populate("groupAdmin", "-password"); 159 | 160 | if (!removed) { 161 | res.status(404); 162 | throw new Error("Chat Not Found"); 163 | } else { 164 | res.json(removed); 165 | } 166 | }); 167 | 168 | // @desc Add user to Group / Leave 169 | // @route PUT /api/chat/groupadd 170 | // @access Protected 171 | const addToGroup = asyncHandler(async (req, res) => { 172 | const { chatId, userId } = req.body; 173 | 174 | // check if the requester is admin 175 | 176 | const added = await Chat.findByIdAndUpdate( 177 | chatId, 178 | { 179 | $push: { users: userId }, // at that time one user can add 180 | }, 181 | { 182 | new: true, 183 | } 184 | ) 185 | .populate("users", "-password") 186 | .populate("groupAdmin", "-password"); 187 | 188 | if (!added) { 189 | res.status(404); 190 | throw new Error("Chat Not Found"); 191 | } else { 192 | res.json(added); 193 | } 194 | }); 195 | 196 | module.exports = { 197 | accessChat, 198 | fetchChats, 199 | createGroupChat, 200 | renameGroup, 201 | addToGroup, 202 | removeFromGroup, 203 | }; 204 | -------------------------------------------------------------------------------- /frontend/src/components/miscellaneous/GroupChatModal.js: -------------------------------------------------------------------------------- 1 | import { 2 | Modal, 3 | ModalOverlay, 4 | ModalContent, 5 | ModalHeader, 6 | ModalFooter, 7 | ModalBody, 8 | ModalCloseButton, 9 | Button, 10 | useDisclosure, 11 | FormControl, 12 | Input, 13 | useToast, 14 | Box, 15 | } from "@chakra-ui/react"; 16 | import axios from "axios"; 17 | import { useState } from "react"; 18 | import { ChatState } from "../../Context/ChatProvider"; 19 | import UserBadgeItem from "../UserAvatar/UserBadgeItem"; 20 | import UserListItem from "../UserAvatar/UserListItem"; 21 | 22 | const GroupChatModal = ({ children }) => { 23 | const { isOpen, onOpen, onClose } = useDisclosure(); 24 | const [groupChatName, setGroupChatName] = useState(); 25 | const [selectedUsers, setSelectedUsers] = useState([]); 26 | const [search, setSearch] = useState(""); 27 | const [searchResult, setSearchResult] = useState([]); 28 | const [loading, setLoading] = useState(false); 29 | const toast = useToast(); 30 | 31 | const { user, chats, setChats } = ChatState(); 32 | 33 | const handleGroup = (userToAdd) => { 34 | // add user to group (during creating new group) 35 | if (selectedUsers.includes(userToAdd)) { 36 | toast({ 37 | title: "User already added", 38 | status: "warning", 39 | duration: 5000, 40 | isClosable: true, 41 | position: "top", 42 | }); 43 | return; 44 | } 45 | 46 | setSelectedUsers([...selectedUsers, userToAdd]); 47 | }; 48 | 49 | const handleSearch = async (query) => { 50 | setSearch(query); 51 | if (!query) { 52 | return; 53 | } 54 | 55 | try { 56 | setLoading(true); 57 | const config = { 58 | headers: { 59 | Authorization: `Bearer ${user.token}`, 60 | }, 61 | }; 62 | const { data } = await axios.get(`/api/user?search=${search}`, config); 63 | 64 | setLoading(false); 65 | setSearchResult(data); 66 | } catch (error) { 67 | toast({ 68 | title: "Error Occured!", 69 | description: "Failed to Load the Search Results", 70 | status: "error", 71 | duration: 5000, 72 | isClosable: true, 73 | position: "bottom-left", 74 | }); 75 | } 76 | }; 77 | 78 | const handleDelete = (delUser) => { 79 | // un-select user that selected for group chat 80 | setSelectedUsers(selectedUsers.filter((sel) => sel._id !== delUser._id)); 81 | }; 82 | 83 | const handleSubmit = async () => { 84 | if (!groupChatName || !selectedUsers) { 85 | toast({ 86 | title: "Please fill all the feilds", 87 | status: "warning", 88 | duration: 5000, 89 | isClosable: true, 90 | position: "top", 91 | }); 92 | return; 93 | } 94 | 95 | try { 96 | const config = { 97 | headers: { 98 | Authorization: `Bearer ${user.token}`, 99 | }, 100 | }; 101 | const { data } = await axios.post( 102 | `/api/chat/group`, 103 | { 104 | name: groupChatName, 105 | users: JSON.stringify(selectedUsers.map((u) => u._id)), 106 | }, 107 | config 108 | ); 109 | setChats([data, ...chats]); 110 | onClose(); 111 | toast({ 112 | title: "New Group Chat Created!", 113 | status: "success", 114 | duration: 5000, 115 | isClosable: true, 116 | position: "bottom", 117 | }); 118 | //re-initalize 119 | setGroupChatName(); 120 | setSelectedUsers([]); 121 | setSearch(); 122 | } catch (error) { 123 | toast({ 124 | title: "Failed to Create the Chat!", 125 | description: error.response.data, 126 | status: "error", 127 | duration: 5000, 128 | isClosable: true, 129 | position: "bottom", 130 | }); 131 | } 132 | }; 133 | 134 | return ( 135 | <> 136 | {children} 137 | 138 | 139 | 140 | 141 | 147 | Create Group Chat 148 | 149 | 150 | 151 | 152 | setGroupChatName(e.target.value)} 156 | /> 157 | 158 | 159 | handleSearch(e.target.value)} 163 | /> 164 | 165 | 166 | {selectedUsers.map((u) => ( 167 | handleDelete(u)} 171 | /> 172 | ))} 173 | 174 | {loading ? ( 175 | // 176 |
Loading...
177 | ) : ( 178 | searchResult 179 | ?.slice(0, 4) 180 | .map((user) => ( 181 | handleGroup(user)} 185 | /> 186 | )) 187 | )} 188 |
189 | 190 | 193 | 194 |
195 |
196 | 197 | ); 198 | }; 199 | 200 | export default GroupChatModal; 201 | -------------------------------------------------------------------------------- /frontend/src/components/Authentication/Signup.js: -------------------------------------------------------------------------------- 1 | import { Button } from "@chakra-ui/button"; 2 | import { FormControl, FormLabel } from "@chakra-ui/form-control"; 3 | import { Input, InputGroup, InputRightElement } from "@chakra-ui/input"; 4 | import { VStack } from "@chakra-ui/layout"; 5 | import { useToast } from "@chakra-ui/toast"; 6 | import axios from "axios"; 7 | import { useState } from "react"; 8 | import { useHistory } from "react-router"; 9 | import { ChatState } from "../../Context/ChatProvider"; 10 | 11 | const Signup = () => { 12 | const [show, setShow] = useState(false); 13 | const handleClick = () => setShow(!show); 14 | const toast = useToast(); 15 | const history = useHistory(); 16 | 17 | const [name, setName] = useState(); 18 | const [email, setEmail] = useState(); 19 | const [confirmpassword, setConfirmpassword] = useState(); 20 | const [password, setPassword] = useState(); 21 | const [pic, setPic] = useState(); 22 | const [picLoading, setPicLoading] = useState(false); 23 | 24 | const { setUser } = ChatState(); 25 | 26 | const submitHandler = async () => { 27 | setPicLoading(true); 28 | if (!name || !email || !password || !confirmpassword) { 29 | toast({ 30 | title: "Please Fill all the Feilds", 31 | status: "warning", 32 | duration: 5000, 33 | isClosable: true, 34 | position: "bottom", 35 | }); 36 | setPicLoading(false); 37 | return; 38 | } 39 | if (password !== confirmpassword) { 40 | toast({ 41 | title: "Passwords Do Not Match", 42 | status: "warning", 43 | duration: 5000, 44 | isClosable: true, 45 | position: "bottom", 46 | }); 47 | return; 48 | } 49 | 50 | try { 51 | const config = { 52 | headers: { 53 | "Content-type": "application/json", 54 | }, 55 | }; 56 | const { data } = await axios.post( 57 | "/api/user", 58 | { 59 | name, 60 | email, 61 | password, 62 | pic, 63 | }, 64 | config 65 | ); 66 | 67 | toast({ 68 | title: "Registration Successful", 69 | status: "success", 70 | duration: 5000, 71 | isClosable: true, 72 | position: "bottom", 73 | }); 74 | setUser(data); 75 | localStorage.setItem("userInfo", JSON.stringify(data)); 76 | setPicLoading(false); 77 | history.push("/chats"); 78 | } catch (error) { 79 | toast({ 80 | title: "Error Occured!", 81 | description: error.response.data.message, 82 | status: "error", 83 | duration: 5000, 84 | isClosable: true, 85 | position: "bottom", 86 | }); 87 | setPicLoading(false); 88 | } 89 | }; 90 | 91 | const postDetails = (pics) => { 92 | setPicLoading(true); 93 | if (pics === undefined) { 94 | toast({ 95 | title: "Please Select an Image!", 96 | status: "warning", 97 | duration: 5000, 98 | isClosable: true, 99 | position: "bottom", 100 | }); 101 | return; 102 | } 103 | 104 | if (pics.type === "image/jpeg" || pics.type === "image/png") { 105 | const data = new FormData(); 106 | data.append("file", pics); 107 | data.append("upload_preset", "JerryChat"); 108 | data.append("cloud_name", "hunterjerry"); 109 | fetch("https://api.cloudinary.com/v1_1/hunterjerry/image/upload", { 110 | method: "post", 111 | body: data, 112 | }) 113 | .then((res) => res.json()) 114 | .then((data) => { 115 | setPic(data.url.toString()); 116 | 117 | setPicLoading(false); 118 | }) 119 | .catch((err) => { 120 | setPicLoading(false); 121 | }); 122 | } else { 123 | toast({ 124 | title: "Please Select an Image!", 125 | status: "warning", 126 | duration: 5000, 127 | isClosable: true, 128 | position: "bottom", 129 | }); 130 | setPicLoading(false); 131 | return; 132 | } 133 | }; 134 | 135 | return ( 136 | 137 | 138 | Name 139 | setName(e.target.value)} 142 | /> 143 | 144 | 145 | Email Address 146 | setEmail(e.target.value)} 150 | /> 151 | 152 | 153 | Password 154 | 155 | setPassword(e.target.value)} 159 | /> 160 | 161 | 164 | 165 | 166 | 167 | 168 | Confirm Password 169 | 170 | setConfirmpassword(e.target.value)} 174 | /> 175 | 176 | 179 | 180 | 181 | 182 | 183 | Upload your Picture 184 | postDetails(e.target.files[0])} 189 | /> 190 | 191 | 200 | 201 | ); 202 | }; 203 | 204 | export default Signup; 205 | -------------------------------------------------------------------------------- /frontend/src/components/miscellaneous/SideDrawer.js: -------------------------------------------------------------------------------- 1 | import { Button } from "@chakra-ui/button"; 2 | import { useDisclosure } from "@chakra-ui/hooks"; 3 | import { Input } from "@chakra-ui/input"; 4 | import { Box, Text } from "@chakra-ui/layout"; 5 | import { 6 | Menu, 7 | MenuButton, 8 | MenuDivider, 9 | MenuItem, 10 | MenuList, 11 | } from "@chakra-ui/menu"; 12 | import { 13 | Drawer, 14 | DrawerBody, 15 | DrawerContent, 16 | DrawerHeader, 17 | DrawerOverlay, 18 | } from "@chakra-ui/modal"; 19 | import { Tooltip } from "@chakra-ui/tooltip"; 20 | import { BellIcon, ChevronDownIcon } from "@chakra-ui/icons"; 21 | import { Avatar } from "@chakra-ui/avatar"; 22 | import { useHistory } from "react-router-dom"; 23 | import { useState } from "react"; 24 | import axios from "axios"; 25 | import { useToast } from "@chakra-ui/toast"; 26 | import ChatLoading from "../ChatLoading"; 27 | import { Spinner } from "@chakra-ui/spinner"; 28 | import ProfileModal from "./ProfileModal"; 29 | import NotificationBadge from "react-notification-badge"; 30 | import { Effect } from "react-notification-badge"; 31 | import { getSender } from "../../config/ChatLogics"; 32 | import UserListItem from "../UserAvatar/UserListItem"; 33 | import { ChatState } from "../../Context/ChatProvider"; 34 | 35 | function SideDrawer() { 36 | const [search, setSearch] = useState(""); 37 | const [searchResult, setSearchResult] = useState([]); 38 | const [loading, setLoading] = useState(false); 39 | const [loadingChat, setLoadingChat] = useState(false); 40 | 41 | const { 42 | setSelectedChat, 43 | user, 44 | notification, 45 | setNotification, 46 | chats, 47 | setChats, 48 | } = ChatState(); 49 | 50 | const toast = useToast(); 51 | const { isOpen, onOpen, onClose } = useDisclosure(); 52 | const history = useHistory(); 53 | 54 | const logoutHandler = () => { 55 | localStorage.removeItem("userInfo"); 56 | history.push("/"); 57 | }; 58 | 59 | const handleSearch = async () => { 60 | if (!search) { 61 | toast({ 62 | title: "Please Enter something in search", 63 | status: "warning", 64 | duration: 5000, 65 | isClosable: true, 66 | position: "top-left", 67 | }); 68 | return; 69 | } 70 | 71 | try { 72 | setLoading(true); 73 | 74 | const config = { 75 | headers: { 76 | Authorization: `Bearer ${user.token}`, 77 | }, 78 | }; 79 | 80 | const { data } = await axios.get(`/api/user?search=${search}`, config); 81 | 82 | setLoading(false); 83 | setSearchResult(data); 84 | } catch (error) { 85 | toast({ 86 | title: "Error Occured!", 87 | description: "Failed to Load the Search Results", 88 | status: "error", 89 | duration: 5000, 90 | isClosable: true, 91 | position: "bottom-left", 92 | }); 93 | } 94 | }; 95 | 96 | const accessChat = async (userId) => { 97 | try { 98 | setLoadingChat(true); 99 | const config = { 100 | headers: { 101 | "Content-type": "application/json", 102 | Authorization: `Bearer ${user.token}`, 103 | }, 104 | }; 105 | const { data } = await axios.post(`/api/chat`, { userId }, config); 106 | 107 | if (!chats.find((c) => c._id === data._id)) setChats([data, ...chats]); 108 | setSelectedChat(data); 109 | setLoadingChat(false); 110 | onClose(); 111 | } catch (error) { 112 | toast({ 113 | title: "Error fetching the chat", 114 | description: error.message, 115 | status: "error", 116 | duration: 5000, 117 | isClosable: true, 118 | position: "bottom-left", 119 | }); 120 | } 121 | }; 122 | 123 | return ( 124 | <> 125 | 134 | 135 | 141 | 142 | 143 | JerryChat 144 | 145 |
146 | 147 | 148 | 152 | 153 | 154 | 155 | {!notification.length && "No New Messages"} 156 | {notification.map((notif) => ( 157 | { 160 | setSelectedChat(notif.chat); 161 | setNotification(notification.filter((n) => n !== notif)); 162 | }} 163 | > 164 | {notif.chat.isGroupChat 165 | ? `New Message in ${notif.chat.chatName}` 166 | : `New Message from ${getSender(user, notif.chat.users)}`} 167 | 168 | ))} 169 | 170 | 171 | 172 | }> 173 | 179 | 180 | 181 | 182 | My Profile{" "} 183 | 184 | 185 | Logout 186 | 187 | 188 |
189 |
190 | 191 | 192 | 193 | 194 | Search Users 195 | 196 | 197 | setSearch(e.target.value)} 202 | /> 203 | 204 | 205 | {loading ? ( 206 | 207 | ) : ( 208 | searchResult?.map((user) => ( 209 | accessChat(user._id)} 213 | /> 214 | )) 215 | )} 216 | {loadingChat && } 217 | 218 | 219 | 220 | 221 | ); 222 | } 223 | 224 | export default SideDrawer; 225 | -------------------------------------------------------------------------------- /frontend/src/components/miscellaneous/UpdateGroupChatModal.js: -------------------------------------------------------------------------------- 1 | import { ViewIcon } from "@chakra-ui/icons"; 2 | import { 3 | Modal, 4 | ModalOverlay, 5 | ModalContent, 6 | ModalHeader, 7 | ModalFooter, 8 | ModalBody, 9 | ModalCloseButton, 10 | Button, 11 | useDisclosure, 12 | FormControl, 13 | Input, 14 | useToast, 15 | Box, 16 | IconButton, 17 | Spinner, 18 | } from "@chakra-ui/react"; 19 | import axios from "axios"; 20 | import { useState } from "react"; 21 | import { ChatState } from "../../Context/ChatProvider"; 22 | import UserBadgeItem from "../UserAvatar/UserBadgeItem"; 23 | import UserListItem from "../UserAvatar/UserListItem"; 24 | 25 | const UpdateGroupChatModal = ({ fetchMessages, fetchAgain, setFetchAgain }) => { 26 | const { isOpen, onOpen, onClose } = useDisclosure(); 27 | const [groupChatName, setGroupChatName] = useState(); 28 | const [search, setSearch] = useState(""); 29 | const [searchResult, setSearchResult] = useState([]); 30 | const [loading, setLoading] = useState(false); 31 | const [renameloading, setRenameLoading] = useState(false); 32 | const toast = useToast(); 33 | 34 | const { selectedChat, setSelectedChat, user } = ChatState(); 35 | 36 | const handleSearch = async (query) => { 37 | setSearch(query); 38 | if (!query) { 39 | return; 40 | } 41 | 42 | try { 43 | setLoading(true); 44 | const config = { 45 | headers: { 46 | Authorization: `Bearer ${user.token}`, 47 | }, 48 | }; 49 | const { data } = await axios.get(`/api/user?search=${search}`, config); 50 | 51 | setLoading(false); 52 | setSearchResult(data); 53 | } catch (error) { 54 | toast({ 55 | title: "Error Occured!", 56 | description: "Failed to Load the Search Results", 57 | status: "error", 58 | duration: 5000, 59 | isClosable: true, 60 | position: "bottom-left", 61 | }); 62 | setLoading(false); 63 | } 64 | }; 65 | 66 | const handleRename = async () => { 67 | if (!groupChatName) return; 68 | 69 | try { 70 | setRenameLoading(true); 71 | const config = { 72 | headers: { 73 | Authorization: `Bearer ${user.token}`, 74 | }, 75 | }; 76 | const { data } = await axios.put( 77 | `/api/chat/rename`, 78 | { 79 | chatId: selectedChat._id, 80 | chatName: groupChatName, 81 | }, 82 | config 83 | ); 84 | 85 | setSelectedChat(data); 86 | setFetchAgain(!fetchAgain); 87 | setRenameLoading(false); 88 | } catch (error) { 89 | toast({ 90 | title: "Error Occured!", 91 | description: error.response.data.message, 92 | status: "error", 93 | duration: 5000, 94 | isClosable: true, 95 | position: "bottom", 96 | }); 97 | setRenameLoading(false); 98 | } 99 | setGroupChatName(""); 100 | }; 101 | 102 | const handleAddUser = async (user1) => { 103 | if (selectedChat.users.find((u) => u._id === user1._id)) { 104 | toast({ 105 | title: "User Already in group!", 106 | status: "error", 107 | duration: 5000, 108 | isClosable: true, 109 | position: "bottom", 110 | }); 111 | return; 112 | } 113 | 114 | if (selectedChat.groupAdmin._id !== user._id) { 115 | toast({ 116 | title: "Only admins can add someone!", 117 | status: "error", 118 | duration: 5000, 119 | isClosable: true, 120 | position: "bottom", 121 | }); 122 | return; 123 | } 124 | 125 | try { 126 | setLoading(true); 127 | const config = { 128 | headers: { 129 | Authorization: `Bearer ${user.token}`, 130 | }, 131 | }; 132 | const { data } = await axios.put( 133 | `/api/chat/groupadd`, 134 | { 135 | chatId: selectedChat._id, 136 | userId: user1._id, 137 | }, 138 | config 139 | ); 140 | 141 | setSelectedChat(data); 142 | setFetchAgain(!fetchAgain); 143 | setLoading(false); 144 | } catch (error) { 145 | toast({ 146 | title: "Error Occured!", 147 | description: error.response.data.message, 148 | status: "error", 149 | duration: 5000, 150 | isClosable: true, 151 | position: "bottom", 152 | }); 153 | setLoading(false); 154 | } 155 | setGroupChatName(""); 156 | }; 157 | 158 | const handleRemove = async (user1) => { 159 | if (selectedChat.groupAdmin._id !== user._id && user1._id !== user._id) { 160 | // admin only can remove other, 161 | toast({ 162 | title: "Only admins can remove someone!", 163 | status: "error", 164 | duration: 5000, 165 | isClosable: true, 166 | position: "bottom", 167 | }); 168 | return; 169 | } 170 | 171 | try { 172 | // admin or other user can leave group,it self 173 | setLoading(true); 174 | const config = { 175 | headers: { 176 | Authorization: `Bearer ${user.token}`, 177 | }, 178 | }; 179 | const { data } = await axios.put( 180 | `/api/chat/groupremove`, 181 | { 182 | chatId: selectedChat._id, 183 | userId: user1._id, 184 | }, 185 | config 186 | ); 187 | 188 | user1._id === user._id ? setSelectedChat() : setSelectedChat(data); 189 | setFetchAgain(!fetchAgain); 190 | fetchMessages(); 191 | setLoading(false); 192 | } catch (error) { 193 | toast({ 194 | title: "Error Occured!", 195 | description: error.response.data.message, 196 | status: "error", 197 | duration: 5000, 198 | isClosable: true, 199 | position: "bottom", 200 | }); 201 | setLoading(false); 202 | } 203 | setGroupChatName(""); 204 | }; 205 | 206 | return ( 207 | <> 208 | } onClick={onOpen} /> 209 | 210 | 211 | 212 | 213 | 219 | {selectedChat.chatName} 220 | 221 | 222 | 223 | 224 | 225 | {selectedChat.users.map((u) => ( 226 | handleRemove(u)} 231 | /> 232 | ))} 233 | 234 | 235 | setGroupChatName(e.target.value)} 240 | /> 241 | 250 | 251 | 252 | handleSearch(e.target.value)} 256 | /> 257 | 258 | 259 | {loading ? ( 260 | 261 | ) : ( 262 | searchResult?.map((user) => ( 263 | handleAddUser(user)} 267 | /> 268 | )) 269 | )} 270 | 271 | 272 | 275 | 276 | 277 | 278 | 279 | ); 280 | }; 281 | 282 | export default UpdateGroupChatModal; 283 | -------------------------------------------------------------------------------- /frontend/src/components/SingleChat.js: -------------------------------------------------------------------------------- 1 | import { FormControl } from "@chakra-ui/form-control"; 2 | import { Input } from "@chakra-ui/input"; 3 | import { Box, Text } from "@chakra-ui/layout"; 4 | import "./styles.css"; 5 | import { IconButton, Spinner, useToast } from "@chakra-ui/react"; 6 | import { getSender, getSenderFull } from "../config/ChatLogics"; 7 | import { useEffect, useState } from "react"; 8 | import axios from "axios"; 9 | import { ArrowBackIcon } from "@chakra-ui/icons"; 10 | import ProfileModal from "./miscellaneous/ProfileModal"; 11 | import ScrollableChat from "./ScrollableChat"; 12 | import Lottie from "react-lottie"; 13 | import animationData from "../animations/typing.json"; 14 | 15 | import { encrypt, decrypt } from "../Context/EncrDecr"; 16 | 17 | import io from "socket.io-client"; 18 | import UpdateGroupChatModal from "./miscellaneous/UpdateGroupChatModal"; 19 | import { ChatState } from "../Context/ChatProvider"; 20 | const ENDPOINT = "https://jerrychat.onrender.com/"; // "https://jerrychat.onrender.com/"; -> After deployment 21 | // const ENDPOINT = "http://127.0.0.1:5000"; // localHost 22 | 23 | var socket, selectedChatCompare; 24 | 25 | const SingleChat = ({ fetchAgain, setFetchAgain }) => { 26 | const [messages, setMessages] = useState([]); 27 | const [loading, setLoading] = useState(false); 28 | const [newMessage, setNewMessage] = useState(""); 29 | const [socketConnected, setSocketConnected] = useState(false); 30 | const [typing, setTyping] = useState(false); 31 | const [istyping, setIsTyping] = useState(false); 32 | const toast = useToast(); 33 | 34 | const defaultOptions = { 35 | loop: true, 36 | autoplay: true, 37 | animationData: animationData, 38 | rendererSettings: { 39 | preserveAspectRatio: "xMidYMid slice", 40 | }, 41 | }; 42 | const { selectedChat, setSelectedChat, user, notification, setNotification } = 43 | ChatState(); 44 | 45 | const fetchMessages = async () => { 46 | if (!selectedChat) return; 47 | 48 | try { 49 | const config = { 50 | headers: { 51 | Authorization: `Bearer ${user.token}`, 52 | }, 53 | }; 54 | 55 | setLoading(true); 56 | 57 | const { data } = await axios.get( 58 | `/api/message/${selectedChat._id}`, 59 | config 60 | ); 61 | // const decryptMessage = decrypt(data); 62 | setMessages(data); 63 | setLoading(false); 64 | 65 | socket.emit("join chat", selectedChat._id); 66 | } catch (error) { 67 | toast({ 68 | title: "Error Occured!", 69 | description: "Failed to Load the Messages", 70 | status: "error", 71 | duration: 5000, 72 | isClosable: true, 73 | position: "bottom", 74 | }); 75 | } 76 | }; 77 | 78 | const sendMessage = async (event) => { 79 | if (event.key === "Enter" && newMessage) { 80 | socket.emit("stop typing", selectedChat._id); 81 | try { 82 | const config = { 83 | headers: { 84 | "Content-type": "application/json", 85 | Authorization: `Bearer ${user.token}`, 86 | }, 87 | }; 88 | console.log(newMessage); 89 | setNewMessage(""); 90 | // first encrypt newMessage 91 | const encryptMessage = encrypt(newMessage, selectedChat._id); 92 | 93 | const { data } = await axios.post( 94 | "/api/message", 95 | { 96 | content: encryptMessage, 97 | chatId: selectedChat, 98 | }, 99 | config 100 | ); 101 | socket.emit("new message", data); // data is encrypted form 102 | setMessages([...messages, data]); 103 | } catch (error) { 104 | toast({ 105 | title: "Error Occured!", 106 | description: "Failed to send the Message", 107 | status: "error", 108 | duration: 5000, 109 | isClosable: true, 110 | position: "bottom", 111 | }); 112 | } 113 | } 114 | }; 115 | 116 | useEffect(() => { 117 | socket = io(ENDPOINT); 118 | socket.emit("setup", user); 119 | socket.on("connected", () => setSocketConnected(true)); 120 | socket.on("typing", () => setIsTyping(true)); 121 | socket.on("stop typing", () => setIsTyping(false)); 122 | 123 | // eslint-disable-next-line 124 | }, []); 125 | 126 | useEffect(() => { 127 | fetchMessages(); 128 | 129 | selectedChatCompare = selectedChat; 130 | // eslint-disable-next-line 131 | }, [selectedChat]); 132 | 133 | useEffect(() => { 134 | socket.on("message recieved", (newMessageRecieved) => { 135 | if ( 136 | !selectedChatCompare || // if chat is not selected or doesn't match current chat 137 | selectedChatCompare._id !== newMessageRecieved.chat._id 138 | ) { 139 | if (!notification.includes(newMessageRecieved)) { 140 | setNotification([newMessageRecieved, ...notification]); 141 | setFetchAgain(!fetchAgain); 142 | } 143 | } else { 144 | // MUSIC PLAYING LOGIN 145 | setMessages([...messages, newMessageRecieved]); 146 | } 147 | }); 148 | }); 149 | 150 | const typingHandler = (e) => { 151 | setNewMessage(e.target.value); 152 | 153 | if (!socketConnected) return; 154 | 155 | if (!typing) { 156 | setTyping(true); 157 | socket.emit("typing", selectedChat._id); 158 | } 159 | let lastTypingTime = new Date().getTime(); 160 | var timerLength = 3000; 161 | setTimeout(() => { 162 | var timeNow = new Date().getTime(); 163 | var timeDiff = timeNow - lastTypingTime; 164 | if (timeDiff >= timerLength && typing) { 165 | socket.emit("stop typing", selectedChat._id); 166 | setTyping(false); 167 | } 168 | }, timerLength); 169 | }; 170 | 171 | return ( 172 | <> 173 | {selectedChat ? ( 174 | <> 175 | 185 | } 188 | onClick={() => setSelectedChat("")} 189 | /> 190 | {messages && 191 | (!selectedChat.isGroupChat ? ( 192 | <> 193 | {getSender(user, selectedChat.users)} 194 | 197 | 198 | ) : ( 199 | <> 200 | {selectedChat.chatName.toUpperCase()} 201 | 206 | 207 | ))} 208 | 209 | 220 | {loading ? ( 221 | 228 | ) : ( 229 |
230 | 231 |
232 | )} 233 | 234 | 240 | {istyping ? ( 241 |
242 | 248 |
249 | ) : ( 250 | <> 251 | )} 252 | 259 |
260 |
261 | 262 | ) : ( 263 | // to get socket.io on same page 264 | 265 | 266 | Click on a user to start chatting 267 | 268 | 269 | )} 270 | 271 | ); 272 | }; 273 | 274 | export default SingleChat; 275 | -------------------------------------------------------------------------------- /frontend/src/animations/typing.json: -------------------------------------------------------------------------------- 1 | { 2 | "v": "5.5.2", 3 | "fr": 60, 4 | "ip": 0, 5 | "op": 104, 6 | "w": 84, 7 | "h": 40, 8 | "nm": "Typing-Indicator", 9 | "ddd": 0, 10 | "assets": [], 11 | "layers": [ 12 | { 13 | "ddd": 0, 14 | "ind": 1, 15 | "ty": 4, 16 | "nm": "Oval 3", 17 | "sr": 1, 18 | "ks": { 19 | "o": { 20 | "a": 1, 21 | "k": [ 22 | { 23 | "i": { "x": [0.643], "y": [1] }, 24 | "o": { "x": [1], "y": [0] }, 25 | "t": 18, 26 | "s": [35], 27 | "e": [100] 28 | }, 29 | { 30 | "i": { "x": [0.099], "y": [1] }, 31 | "o": { "x": [0.129], "y": [0] }, 32 | "t": 33, 33 | "s": [100], 34 | "e": [35] 35 | }, 36 | { 37 | "i": { "x": [0.833], "y": [1] }, 38 | "o": { "x": [0.167], "y": [0] }, 39 | "t": 65, 40 | "s": [35], 41 | "e": [35] 42 | }, 43 | { "t": 71 } 44 | ], 45 | "ix": 11, 46 | "x": "var $bm_rt;\n$bm_rt = loopOut('cycle', 0);" 47 | }, 48 | "r": { "a": 0, "k": 0, "ix": 10 }, 49 | "p": { "a": 0, "k": [61, 20, 0], "ix": 2 }, 50 | "a": { "a": 0, "k": [0, 0, 0], "ix": 1 }, 51 | "s": { 52 | "a": 1, 53 | "k": [ 54 | { 55 | "i": { "x": [0.667, 0.667, 0.667], "y": [1, 1, 1] }, 56 | "o": { "x": [1, 1, 0.333], "y": [0, 0, 0] }, 57 | "t": 18, 58 | "s": [100, 100, 100], 59 | "e": [140, 140, 100] 60 | }, 61 | { 62 | "i": { "x": [0.032, 0.032, 0.667], "y": [1, 1, 1] }, 63 | "o": { "x": [0.217, 0.217, 0.333], "y": [0, 0, 0] }, 64 | "t": 33, 65 | "s": [140, 140, 100], 66 | "e": [100, 100, 100] 67 | }, 68 | { 69 | "i": { "x": [0.833, 0.833, 0.833], "y": [1, 1, 1] }, 70 | "o": { "x": [0.167, 0.167, 0.167], "y": [0, 0, 0] }, 71 | "t": 65, 72 | "s": [100, 100, 100], 73 | "e": [100, 100, 100] 74 | }, 75 | { "t": 71 } 76 | ], 77 | "ix": 6, 78 | "x": "var $bm_rt;\n$bm_rt = loopOut('cycle', 0);" 79 | } 80 | }, 81 | "ao": 0, 82 | "shapes": [ 83 | { 84 | "ty": "gr", 85 | "it": [ 86 | { 87 | "d": 1, 88 | "ty": "el", 89 | "s": { "a": 0, "k": [12, 12], "ix": 2 }, 90 | "p": { "a": 0, "k": [0, 0], "ix": 3 }, 91 | "nm": "Ellipse Path 1", 92 | "mn": "ADBE Vector Shape - Ellipse", 93 | "hd": false 94 | }, 95 | { 96 | "ty": "fl", 97 | "c": { 98 | "a": 0, 99 | "k": [0.847000002861, 0.847000002861, 0.847000002861, 1], 100 | "ix": 4 101 | }, 102 | "o": { "a": 0, "k": 100, "ix": 5 }, 103 | "r": 1, 104 | "bm": 0, 105 | "nm": "Fill 1", 106 | "mn": "ADBE Vector Graphic - Fill", 107 | "hd": false 108 | }, 109 | { 110 | "ty": "tr", 111 | "p": { "a": 0, "k": [0, 0], "ix": 2 }, 112 | "a": { "a": 0, "k": [0, 0], "ix": 1 }, 113 | "s": { "a": 0, "k": [100, 100], "ix": 3 }, 114 | "r": { "a": 0, "k": 0, "ix": 6 }, 115 | "o": { "a": 0, "k": 100, "ix": 7 }, 116 | "sk": { "a": 0, "k": 0, "ix": 4 }, 117 | "sa": { "a": 0, "k": 0, "ix": 5 }, 118 | "nm": "Transform" 119 | } 120 | ], 121 | "nm": "Oval 3", 122 | "np": 2, 123 | "cix": 2, 124 | "bm": 0, 125 | "ix": 1, 126 | "mn": "ADBE Vector Group", 127 | "hd": false 128 | } 129 | ], 130 | "ip": 0, 131 | "op": 3600, 132 | "st": 0, 133 | "bm": 0 134 | }, 135 | { 136 | "ddd": 0, 137 | "ind": 2, 138 | "ty": 4, 139 | "nm": "Oval 2", 140 | "sr": 1, 141 | "ks": { 142 | "o": { 143 | "a": 1, 144 | "k": [ 145 | { 146 | "i": { "x": [0.667], "y": [1] }, 147 | "o": { "x": [1], "y": [0] }, 148 | "t": 9, 149 | "s": [35], 150 | "e": [98] 151 | }, 152 | { 153 | "i": { "x": [0.023], "y": [1] }, 154 | "o": { "x": [0.179], "y": [0] }, 155 | "t": 24, 156 | "s": [98], 157 | "e": [35] 158 | }, 159 | { 160 | "i": { "x": [0.833], "y": [1] }, 161 | "o": { "x": [0.167], "y": [0] }, 162 | "t": 56, 163 | "s": [35], 164 | "e": [35] 165 | }, 166 | { "t": 62 } 167 | ], 168 | "ix": 11, 169 | "x": "var $bm_rt;\n$bm_rt = loopOut('cycle', 0);" 170 | }, 171 | "r": { "a": 0, "k": 0, "ix": 10 }, 172 | "p": { "a": 0, "k": [41, 20, 0], "ix": 2 }, 173 | "a": { "a": 0, "k": [0, 0, 0], "ix": 1 }, 174 | "s": { 175 | "a": 1, 176 | "k": [ 177 | { 178 | "i": { "x": [0.654, 0.654, 0.667], "y": [1, 1, 1] }, 179 | "o": { "x": [1, 1, 0.333], "y": [0, 0, 0] }, 180 | "t": 9, 181 | "s": [100, 100, 100], 182 | "e": [140, 140, 100] 183 | }, 184 | { 185 | "i": { "x": [0.11, 0.11, 0.667], "y": [1, 1, 1] }, 186 | "o": { "x": [0.205, 0.205, 0.333], "y": [0, 0, 0] }, 187 | "t": 24, 188 | "s": [140, 140, 100], 189 | "e": [100, 100, 100] 190 | }, 191 | { 192 | "i": { "x": [0.833, 0.833, 0.833], "y": [1, 1, 1] }, 193 | "o": { "x": [0.167, 0.167, 0.167], "y": [0, 0, 0] }, 194 | "t": 56, 195 | "s": [100, 100, 100], 196 | "e": [100, 100, 100] 197 | }, 198 | { "t": 62 } 199 | ], 200 | "ix": 6, 201 | "x": "var $bm_rt;\n$bm_rt = loopOut('cycle', 0);" 202 | } 203 | }, 204 | "ao": 0, 205 | "shapes": [ 206 | { 207 | "ty": "gr", 208 | "it": [ 209 | { 210 | "d": 1, 211 | "ty": "el", 212 | "s": { "a": 0, "k": [12, 12], "ix": 2 }, 213 | "p": { "a": 0, "k": [0, 0], "ix": 3 }, 214 | "nm": "Ellipse Path 1", 215 | "mn": "ADBE Vector Shape - Ellipse", 216 | "hd": false 217 | }, 218 | { 219 | "ty": "fl", 220 | "c": { 221 | "a": 0, 222 | "k": [0.847000002861, 0.847000002861, 0.847000002861, 1], 223 | "ix": 4 224 | }, 225 | "o": { "a": 0, "k": 100, "ix": 5 }, 226 | "r": 1, 227 | "bm": 0, 228 | "nm": "Fill 1", 229 | "mn": "ADBE Vector Graphic - Fill", 230 | "hd": false 231 | }, 232 | { 233 | "ty": "tr", 234 | "p": { "a": 0, "k": [0, 0], "ix": 2 }, 235 | "a": { "a": 0, "k": [0, 0], "ix": 1 }, 236 | "s": { "a": 0, "k": [100, 100], "ix": 3 }, 237 | "r": { "a": 0, "k": 0, "ix": 6 }, 238 | "o": { "a": 0, "k": 100, "ix": 7 }, 239 | "sk": { "a": 0, "k": 0, "ix": 4 }, 240 | "sa": { "a": 0, "k": 0, "ix": 5 }, 241 | "nm": "Transform" 242 | } 243 | ], 244 | "nm": "Oval 2", 245 | "np": 2, 246 | "cix": 2, 247 | "bm": 0, 248 | "ix": 1, 249 | "mn": "ADBE Vector Group", 250 | "hd": false 251 | } 252 | ], 253 | "ip": 0, 254 | "op": 3600, 255 | "st": 0, 256 | "bm": 0 257 | }, 258 | { 259 | "ddd": 0, 260 | "ind": 3, 261 | "ty": 4, 262 | "nm": "Oval 1", 263 | "sr": 1, 264 | "ks": { 265 | "o": { 266 | "a": 1, 267 | "k": [ 268 | { 269 | "i": { "x": [0.667], "y": [1] }, 270 | "o": { "x": [1], "y": [0] }, 271 | "t": 0, 272 | "s": [35], 273 | "e": [100] 274 | }, 275 | { 276 | "i": { "x": [0.067], "y": [1] }, 277 | "o": { "x": [0.125], "y": [0] }, 278 | "t": 15, 279 | "s": [100], 280 | "e": [35] 281 | }, 282 | { 283 | "i": { "x": [0.833], "y": [1] }, 284 | "o": { "x": [0.167], "y": [0] }, 285 | "t": 47, 286 | "s": [35], 287 | "e": [35] 288 | }, 289 | { "t": 53 } 290 | ], 291 | "ix": 11, 292 | "x": "var $bm_rt;\n$bm_rt = loopOut('cycle', 0);" 293 | }, 294 | "r": { "a": 0, "k": 0, "ix": 10 }, 295 | "p": { "a": 0, "k": [21, 20, 0], "ix": 2 }, 296 | "a": { "a": 0, "k": [0, 0, 0], "ix": 1 }, 297 | "s": { 298 | "a": 1, 299 | "k": [ 300 | { 301 | "i": { "x": [0.673, 0.673, 0.667], "y": [1, 1, 1] }, 302 | "o": { "x": [1, 1, 0.333], "y": [0, 0, 0] }, 303 | "t": 0, 304 | "s": [100, 100, 100], 305 | "e": [140, 140, 100] 306 | }, 307 | { 308 | "i": { "x": [0.049, 0.049, 0.667], "y": [1, 1, 1] }, 309 | "o": { "x": [0.198, 0.198, 0.333], "y": [0, 0, 0] }, 310 | "t": 15, 311 | "s": [140, 140, 100], 312 | "e": [100, 100, 100] 313 | }, 314 | { 315 | "i": { "x": [0.833, 0.833, 0.833], "y": [1, 1, 1] }, 316 | "o": { "x": [0.167, 0.167, 0.167], "y": [0, 0, 0] }, 317 | "t": 47, 318 | "s": [100, 100, 100], 319 | "e": [100, 100, 100] 320 | }, 321 | { "t": 53 } 322 | ], 323 | "ix": 6, 324 | "x": "var $bm_rt;\n$bm_rt = loopOut('cycle', 0);" 325 | } 326 | }, 327 | "ao": 0, 328 | "shapes": [ 329 | { 330 | "ty": "gr", 331 | "it": [ 332 | { 333 | "d": 1, 334 | "ty": "el", 335 | "s": { "a": 0, "k": [12, 12], "ix": 2 }, 336 | "p": { "a": 0, "k": [0, 0], "ix": 3 }, 337 | "nm": "Ellipse Path 1", 338 | "mn": "ADBE Vector Shape - Ellipse", 339 | "hd": false 340 | }, 341 | { 342 | "ty": "fl", 343 | "c": { 344 | "a": 0, 345 | "k": [0.847000002861, 0.847000002861, 0.847000002861, 1], 346 | "ix": 4 347 | }, 348 | "o": { "a": 0, "k": 100, "ix": 5 }, 349 | "r": 1, 350 | "bm": 0, 351 | "nm": "Fill 1", 352 | "mn": "ADBE Vector Graphic - Fill", 353 | "hd": false 354 | }, 355 | { 356 | "ty": "tr", 357 | "p": { "a": 0, "k": [0, 0], "ix": 2 }, 358 | "a": { "a": 0, "k": [0, 0], "ix": 1 }, 359 | "s": { "a": 0, "k": [100, 100], "ix": 3 }, 360 | "r": { "a": 0, "k": 0, "ix": 6 }, 361 | "o": { "a": 0, "k": 100, "ix": 7 }, 362 | "sk": { "a": 0, "k": 0, "ix": 4 }, 363 | "sa": { "a": 0, "k": 0, "ix": 5 }, 364 | "nm": "Transform" 365 | } 366 | ], 367 | "nm": "Oval 1", 368 | "np": 2, 369 | "cix": 2, 370 | "bm": 0, 371 | "ix": 1, 372 | "mn": "ADBE Vector Group", 373 | "hd": false 374 | } 375 | ], 376 | "ip": 0, 377 | "op": 3600, 378 | "st": 0, 379 | "bm": 0 380 | }, 381 | { 382 | "ddd": 0, 383 | "ind": 4, 384 | "ty": 4, 385 | "nm": "BG", 386 | "sr": 1, 387 | "ks": { 388 | "o": { "a": 0, "k": 100, "ix": 11 }, 389 | "r": { "a": 0, "k": 0, "ix": 10 }, 390 | "p": { "a": 0, "k": [42, 20, 0], "ix": 2 }, 391 | "a": { "a": 0, "k": [0, 0, 0], "ix": 1 }, 392 | "s": { "a": 0, "k": [100, 100, 100], "ix": 6 } 393 | }, 394 | "ao": 0, 395 | "shapes": [ 396 | { 397 | "ty": "gr", 398 | "it": [ 399 | { 400 | "ind": 0, 401 | "ty": "sh", 402 | "ix": 1, 403 | "ks": { 404 | "a": 0, 405 | "k": { 406 | "i": [ 407 | [0, 0], 408 | [0, 0], 409 | [0, 0], 410 | [0, 0] 411 | ], 412 | "o": [ 413 | [0, 0], 414 | [0, 0], 415 | [0, 0], 416 | [0, 0] 417 | ], 418 | "v": [ 419 | [-42, -20], 420 | [42, -20], 421 | [42, 20], 422 | [-42, 20] 423 | ], 424 | "c": true 425 | }, 426 | "ix": 2 427 | }, 428 | "nm": "Path 1", 429 | "mn": "ADBE Vector Shape - Group", 430 | "hd": false 431 | }, 432 | { 433 | "ty": "rd", 434 | "nm": "Round Corners 1", 435 | "r": { "a": 0, "k": 20, "ix": 1 }, 436 | "ix": 2, 437 | "mn": "ADBE Vector Filter - RC", 438 | "hd": false 439 | }, 440 | { 441 | "ty": "fl", 442 | "c": { 443 | "a": 0, 444 | "k": [0.96078401804, 0.96078401804, 0.96078401804, 1], 445 | "ix": 4 446 | }, 447 | "o": { "a": 0, "k": 100, "ix": 5 }, 448 | "r": 1, 449 | "bm": 0, 450 | "nm": "Fill 1", 451 | "mn": "ADBE Vector Graphic - Fill", 452 | "hd": false 453 | }, 454 | { 455 | "ty": "tr", 456 | "p": { "a": 0, "k": [0, 0], "ix": 2 }, 457 | "a": { "a": 0, "k": [0, 0], "ix": 1 }, 458 | "s": { "a": 0, "k": [100, 100], "ix": 3 }, 459 | "r": { "a": 0, "k": 0, "ix": 6 }, 460 | "o": { "a": 0, "k": 100, "ix": 7 }, 461 | "sk": { "a": 0, "k": 0, "ix": 4 }, 462 | "sa": { "a": 0, "k": 0, "ix": 5 }, 463 | "nm": "Transform" 464 | } 465 | ], 466 | "nm": "BG", 467 | "np": 3, 468 | "cix": 2, 469 | "bm": 0, 470 | "ix": 1, 471 | "mn": "ADBE Vector Group", 472 | "hd": false 473 | } 474 | ], 475 | "ip": 0, 476 | "op": 3600, 477 | "st": 0, 478 | "bm": 0 479 | } 480 | ], 481 | "markers": [] 482 | } 483 | --------------------------------------------------------------------------------