├── .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 |
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 | 
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 | 
113 |
114 | - Real Time Chatting with Typing indicators
115 | 
116 |
117 | - One to One chat
118 | 
119 |
120 | - Search Users
121 | 
122 |
123 | - Create Group Chats
124 | 
125 |
126 | - Notifications
127 | 
128 |
129 | - Add or Remove users from group
130 | 
131 |
132 | - View Other user Profile
133 | 
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 | }
77 | >
78 | New Group Chat
79 |
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 |
171 |
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 |
--------------------------------------------------------------------------------