arabdullah commited on
Commit
a0fda44
·
verified ·
1 Parent(s): 2851dfc

@ARAbdullaSL

Browse files
This view is limited to 50 files because it contains too many changes.   See raw diff
Files changed (50) hide show
  1. .env +6 -0
  2. .gitattributes +1 -0
  3. Dockerfile +28 -0
  4. app.js +37 -0
  5. client/.env +7 -0
  6. client/package-lock.json +0 -0
  7. client/package.json +57 -0
  8. client/postcss.config.js +6 -0
  9. client/public/favicon.svg +1 -0
  10. client/public/index.html +49 -0
  11. client/public/logo192.png +0 -0
  12. client/public/logo512.png +0 -0
  13. client/public/manifest.json +25 -0
  14. client/public/robots.txt +3 -0
  15. client/src/App.jsx +49 -0
  16. client/src/App.test.js +8 -0
  17. client/src/AppRoutes.js +0 -0
  18. client/src/assets/Receiver Call Request Tone.mp3 +0 -0
  19. client/src/assets/Sender Call Request Tone.mp3 +3 -0
  20. client/src/assets/chat-bg-dark.jpg +0 -0
  21. client/src/assets/chat-bg-light.jpg +0 -0
  22. client/src/components/globals/CTAIconWrapper.jsx +14 -0
  23. client/src/components/globals/DeleteChat.jsx +50 -0
  24. client/src/components/globals/DeleteContact.jsx +59 -0
  25. client/src/components/globals/FormField.jsx +47 -0
  26. client/src/components/globals/Header.jsx +7 -0
  27. client/src/components/globals/IconWrapper.jsx +16 -0
  28. client/src/components/globals/Image.jsx +18 -0
  29. client/src/components/globals/MessageCheck.jsx +54 -0
  30. client/src/components/globals/Modal.jsx +45 -0
  31. client/src/components/globals/ModalChild.jsx +14 -0
  32. client/src/components/globals/NewContactForm.jsx +130 -0
  33. client/src/components/globals/Notification.jsx +59 -0
  34. client/src/components/globals/Overlay.jsx +27 -0
  35. client/src/components/globals/Sidebar.jsx +25 -0
  36. client/src/components/globals/Spinner.jsx +22 -0
  37. client/src/components/globals/VideoCallModal.jsx +142 -0
  38. client/src/components/globals/VoiceCallModal.jsx +137 -0
  39. client/src/components/pages/Authentication/Login.jsx +87 -0
  40. client/src/components/pages/Authentication/Register.jsx +146 -0
  41. client/src/components/pages/Chat/ActionsModal.jsx +106 -0
  42. client/src/components/pages/Chat/AttachFileModal.jsx +52 -0
  43. client/src/components/pages/Chat/AttachFileOrRecordDuration.jsx +53 -0
  44. client/src/components/pages/Chat/BubbleTail.jsx +22 -0
  45. client/src/components/pages/Chat/CTAButtons.jsx +194 -0
  46. client/src/components/pages/Chat/CallMessage.jsx +120 -0
  47. client/src/components/pages/Chat/ChatHeader.jsx +119 -0
  48. client/src/components/pages/Chat/DayMessages.jsx +57 -0
  49. client/src/components/pages/Chat/EmojiModal.jsx +39 -0
  50. client/src/components/pages/Chat/Message.jsx +74 -0
.env ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ MONGO_URI=mongodb+srv://user:321465@cluster0.7jcofhm.mongodb.net/?retryWrites=true&w=majority&appName=Cluster0
2
+ CLOUDINARY_NAME=dtl9ly6ry
3
+ CLOUDINARY_API_KEY=936793283939455
4
+ CLOUDINARY_API_SECRET=YdqS9mHradZrHx6gJYOVTVEu8oI
5
+ JWT_SECRET_KEY=AIzaSyA9ynkASznKmmLRppnQpTIRipJe9RK34NA
6
+ JWT_EXPIRES_IN=1200d
.gitattributes CHANGED
@@ -33,3 +33,4 @@ saved_model/**/* filter=lfs diff=lfs merge=lfs -text
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
 
 
33
  *.zip filter=lfs diff=lfs merge=lfs -text
34
  *.zst filter=lfs diff=lfs merge=lfs -text
35
  *tfevents* filter=lfs diff=lfs merge=lfs -text
36
+ client/src/assets/Sender[[:space:]]Call[[:space:]]Request[[:space:]]Tone.mp3 filter=lfs diff=lfs merge=lfs -text
Dockerfile ADDED
@@ -0,0 +1,28 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # Base image
2
+ FROM quay.io/suhailtechinfo/suhail-v2
3
+
4
+ # Create the app directory with correct permissions
5
+ RUN mkdir -p /home/app && chown -R node:node /home/app
6
+
7
+ # Set the working directory
8
+ WORKDIR /home/app
9
+
10
+ # Copy the entire app directory into the container with the correct ownership
11
+ COPY --chown=node:node . .
12
+
13
+ # Install dependencies
14
+ RUN npm install || yarn install
15
+
16
+ # Create the start.sh script with the correct permissions
17
+ RUN echo '#!/bin/sh\nnpm start' > start.sh \
18
+ && chmod +x start.sh \
19
+ && chown node:node start.sh
20
+
21
+ # Switch to the 'node' user
22
+ USER node
23
+
24
+ # Expose the port
25
+ EXPOSE 3000
26
+
27
+ # Run the start.sh script
28
+ CMD ["sh", "start.sh"]
app.js ADDED
@@ -0,0 +1,37 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const express = require("express");
2
+ const cookieParser = require("cookie-parser");
3
+ const cors = require("cors");
4
+
5
+ const app = express();
6
+ const authRouter = require("./routers/authRouter");
7
+ const contactsRouter = require("./routers/contactsRouter");
8
+ const chatRoomRouter = require("./routers/chatRoomRouter");
9
+ const profileRouter = require("./routers/profileRouter");
10
+ const uploadRouter = require("./routers/uploadRouter");
11
+ const ReqError = require("./utilities/ReqError");
12
+ const errorController = require("./controllers/errorController");
13
+
14
+ app.use(express.json({ limit: "50mb" }));
15
+ app.use(cookieParser());
16
+ app.use(cors());
17
+
18
+ // Routes
19
+ app.use("/api/user", authRouter);
20
+
21
+ // Protector
22
+ app.use("/api/*", (req, res, next) => {
23
+ if (!req.cookies.userId)
24
+ return next(new ReqError(400, "You are not logged in"));
25
+
26
+ next();
27
+ });
28
+
29
+ app.use("/api/contacts", contactsRouter);
30
+ app.use("/api/profile", profileRouter);
31
+ app.use("/api/chatRoom", chatRoomRouter);
32
+ app.use("/api/upload", uploadRouter);
33
+
34
+ // Error handle middleware
35
+ app.use(errorController);
36
+
37
+ module.exports = app;
client/.env ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ REACT_APP_BOT_ID=63e2b0bdc3f617f799a8b6b1
2
+ REACT_APP_BOT_USERNAME=EddieBot
3
+ REACT_APP_BOT_NAME=Telegram Bot
4
+ REACT_APP_BOT_URL=https://www.botlibre.com/rest/json/chat
5
+ REACT_APP_BOT_APPLICATION_ID=2496722781764642402
6
+ REACT_APP_BOT_APPLICATION_INSTANCE=12332376
7
+ REACT_APP_CLOUDINARY_NAME=dlanhtzbw
client/package-lock.json ADDED
The diff for this file is too large to render. See raw diff
 
client/package.json ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "client",
3
+ "proxy": "http://localhost:4000",
4
+ "version": "0.1.0",
5
+ "private": true,
6
+ "dependencies": {
7
+ "@reduxjs/toolkit": "^1.8.6",
8
+ "@testing-library/jest-dom": "^5.16.5",
9
+ "@testing-library/react": "^13.4.0",
10
+ "@testing-library/user-event": "^13.5.0",
11
+ "cloudinary-react": "^1.8.1",
12
+ "emoji-picker-react": "^4.3.0",
13
+ "formik": "^2.2.9",
14
+ "framer-motion": "^7.5.3",
15
+ "process": "^0.11.10",
16
+ "react": "^18.2.0",
17
+ "react-dom": "^18.2.0",
18
+ "react-jplayer": "^7.1.3",
19
+ "react-media-recorder": "^1.6.6",
20
+ "react-redux": "^8.0.4",
21
+ "react-router-dom": "^6.4.2",
22
+ "react-scripts": "5.0.1",
23
+ "simple-peer": "^9.11.1",
24
+ "socket.io-client": "^4.5.3",
25
+ "web-vitals": "^2.1.4",
26
+ "yup": "^0.32.11"
27
+ },
28
+ "scripts": {
29
+ "start": "npx react-scripts start",
30
+ "build": "npx react-scripts build",
31
+ "test": "npx react-scripts test",
32
+ "eject": "npx react-scripts eject"
33
+ },
34
+ "eslintConfig": {
35
+ "extends": [
36
+ "react-app",
37
+ "react-app/jest"
38
+ ]
39
+ },
40
+ "browserslist": {
41
+ "production": [
42
+ ">0.2%",
43
+ "not dead",
44
+ "not op_mini all"
45
+ ],
46
+ "development": [
47
+ "last 1 chrome version",
48
+ "last 1 firefox version",
49
+ "last 1 safari version"
50
+ ]
51
+ },
52
+ "devDependencies": {
53
+ "autoprefixer": "^10.4.12",
54
+ "postcss": "^8.4.17",
55
+ "tailwindcss": "^3.1.8"
56
+ }
57
+ }
client/postcss.config.js ADDED
@@ -0,0 +1,6 @@
 
 
 
 
 
 
 
1
+ module.exports = {
2
+ plugins: {
3
+ tailwindcss: {},
4
+ autoprefixer: {},
5
+ },
6
+ }
client/public/favicon.svg ADDED
client/public/index.html ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="utf-8" />
5
+ <link
6
+ rel="icon"
7
+ href="%PUBLIC_URL%/favicon.svg"
8
+ sizes="any"
9
+ type="image/svg+xml"
10
+ />
11
+ <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
12
+ <meta name="viewport" content="width=device-width, initial-scale=1" />
13
+ <meta name="theme-color" content="#000000" />
14
+ <meta
15
+ name="description"
16
+ content="Web site created using create-react-app"
17
+ />
18
+ <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
19
+ <!--
20
+ manifest.json provides metadata used when your web app is installed on a
21
+ user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
22
+ -->
23
+ <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
24
+ <!--
25
+ Notice the use of %PUBLIC_URL% in the tags above.
26
+ It will be replaced with the URL of the `public` folder during the build.
27
+ Only files inside the `public` folder can be referenced from the HTML.
28
+
29
+ Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
30
+ work correctly both with client-side routing and a non-root public URL.
31
+ Learn how to configure a non-root public URL by running `npm run build`.
32
+ -->
33
+ <title>Telegram Web</title>
34
+ </head>
35
+ <body>
36
+ <noscript>You need to enable JavaScript to run this app.</noscript>
37
+ <div id="root"></div>
38
+ <!--
39
+ This HTML file is a template.
40
+ If you open it directly in the browser, you will see an empty page.
41
+
42
+ You can add webfonts, meta tags, or analytics to this file.
43
+ The build step will place the bundled scripts into the <body> tag.
44
+
45
+ To begin the development, run `npm start` or `yarn start`.
46
+ To create a production bundle, use `npm run build` or `yarn build`.
47
+ -->
48
+ </body>
49
+ </html>
client/public/logo192.png ADDED
client/public/logo512.png ADDED
client/public/manifest.json ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "short_name": "React App",
3
+ "name": "Create React App Sample",
4
+ "icons": [
5
+ {
6
+ "src": "favicon.ico",
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
+ }
client/public/robots.txt ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ # https://www.robotstxt.org/robotstxt.html
2
+ User-agent: *
3
+ Disallow:
client/src/App.jsx ADDED
@@ -0,0 +1,49 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import DeleteChat from "./components/globals/DeleteChat";
2
+ import DeleteContact from "./components/globals/DeleteContact";
3
+ import NewContactForm from "./components/globals/NewContactForm";
4
+ import Sidebar from "./components/globals/Sidebar";
5
+ import VoiceCallModal from "./components/globals/VoiceCallModal";
6
+ import VideoCallModal from "./components/globals/VideoCallModal";
7
+ import Authentication from "./pages/Authentication";
8
+ import Chat from "./pages/Chat";
9
+ import UserProfile from "./pages/UserProfile";
10
+ import useInit from "./hooks/useInit";
11
+ import Notification from "./components/globals/Notification";
12
+ import { useSelector } from "react-redux";
13
+ import useAppHeight from "./hooks/useAppHeight";
14
+
15
+ function App() {
16
+ // Initialize application
17
+ const { loggedIn } = useInit();
18
+ const modalType = useSelector((state) => state.modalReducer.type);
19
+ useAppHeight();
20
+
21
+ return (
22
+ <div className="w-full h-full flex overflow-hidden bg-primary relative">
23
+ {loggedIn && (
24
+ <>
25
+ {/* Sidebar to show ChatList, Contacts, Settings Page */}
26
+ <Sidebar />
27
+ {/* Chat to show messages */}
28
+ <Chat />
29
+ <UserProfile />
30
+ </>
31
+ )}
32
+
33
+ {!loggedIn && <Authentication />}
34
+
35
+ {/* Notification */}
36
+ <Notification />
37
+
38
+ {/* Modals */}
39
+ <DeleteChat />
40
+ <DeleteContact />
41
+ <NewContactForm />
42
+
43
+ {modalType === "voiceCallModal" && <VoiceCallModal />}
44
+ {modalType === "videoCallModal" && <VideoCallModal />}
45
+ </div>
46
+ );
47
+ }
48
+
49
+ export default App;
client/src/App.test.js ADDED
@@ -0,0 +1,8 @@
 
 
 
 
 
 
 
 
 
1
+ import { render, screen } from '@testing-library/react';
2
+ import App from './App';
3
+
4
+ test('renders learn react link', () => {
5
+ render(<App />);
6
+ const linkElement = screen.getByText(/learn react/i);
7
+ expect(linkElement).toBeInTheDocument();
8
+ });
client/src/AppRoutes.js ADDED
File without changes
client/src/assets/Receiver Call Request Tone.mp3 ADDED
Binary file (287 kB). View file
 
client/src/assets/Sender Call Request Tone.mp3 ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:ed43a89773e88b65bbb501cde8e0aa14cbc43611afafb919841a50cb913661bd
3
+ size 1203108
client/src/assets/chat-bg-dark.jpg ADDED
client/src/assets/chat-bg-light.jpg ADDED
client/src/components/globals/CTAIconWrapper.jsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ function CTAIconWrapper({ className, children, onClick }) {
4
+ return (
5
+ <div
6
+ onClick={onClick}
7
+ className={`bg-cta-icon flex items-center justify-center w-[5.5rem] h-[5.5rem] rounded-full ${className}`}
8
+ >
9
+ {children}
10
+ </div>
11
+ );
12
+ }
13
+
14
+ export default CTAIconWrapper;
client/src/components/globals/DeleteChat.jsx ADDED
@@ -0,0 +1,50 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useDispatch, useSelector } from "react-redux";
3
+ import { modalActions } from "../../store/modalSlice";
4
+ import Modal from "./Modal";
5
+ import useFetch from "../../hooks/useFetch";
6
+ import { chatActions } from "../../store/chatSlice";
7
+ import useSocket from "../../hooks/useSocket";
8
+
9
+ function DeleteChat() {
10
+ const dispatch = useDispatch();
11
+ const chatData = useSelector((state) => state.modalReducer.payload.chatData);
12
+ const { socketEmit } = useSocket();
13
+
14
+ const deleteChatRoom = () => {
15
+ socketEmit("user:chatRoomClear", {
16
+ chatRoomId: chatData?.chatRoomId || chatData?._id,
17
+ });
18
+ dispatch(chatActions.setChatUnactive());
19
+ };
20
+
21
+ return (
22
+ <Modal
23
+ onClick={() => dispatch(modalActions.closeModal())}
24
+ typeValue="deleteChatModal"
25
+ className="w-[30rem] !px-[2rem] pb-[2rem]"
26
+ canOverlayClose={true}
27
+ >
28
+ <h2 className="font-semibold text-[2rem]">Discard Chat</h2>
29
+ <p className="">
30
+ Are you sure you want to delete the chat with{" "}
31
+ {chatData?.profile?.name || chatData?.chatProfile?.username}
32
+ </p>
33
+ <div className="flex items-center justify-center gap-[2rem] mt-[1rem]">
34
+ <button className="text-cta-icon rounded-md font-semibold uppercase p-[1rem] hover:bg-secondary-light-text">
35
+ Cancel
36
+ </button>
37
+ <button
38
+ onClick={() => {
39
+ deleteChatRoom();
40
+ }}
41
+ className="text-danger rounded-md font-semibold uppercase p-[1rem] hover:bg-secondary-light-text"
42
+ >
43
+ Delete Chat
44
+ </button>
45
+ </div>
46
+ </Modal>
47
+ );
48
+ }
49
+
50
+ export default DeleteChat;
client/src/components/globals/DeleteContact.jsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useDispatch, useSelector } from "react-redux";
3
+ import { modalActions } from "../../store/modalSlice";
4
+ import Modal from "./Modal";
5
+ import useFetch from "../../hooks/useFetch";
6
+ import { contactsActions } from "../../store/contactsSlice";
7
+ import { chatActions } from "../../store/chatSlice";
8
+ import { userProfileActions } from "../../store/userProfileSlice";
9
+
10
+ function DeleteContact() {
11
+ const dispatch = useDispatch();
12
+ const chatRoomId = useSelector(
13
+ (state) => state.chatReducer.currentChatRoom._id
14
+ );
15
+ const contactData = useSelector((state) => state.modalReducer.payload);
16
+ const { reqFn: deleteContact } = useFetch({
17
+ method: "DELETE",
18
+ url: "/contacts",
19
+ });
20
+
21
+ const deleteContactHandler = async () => {
22
+ // Hide profile
23
+ dispatch(userProfileActions.hideProfile());
24
+ // Delete contact from database
25
+ await deleteContact({ username: contactData?.profile?.username });
26
+ // Remove contact from redux store
27
+ dispatch(contactsActions.removeContact({ id: contactData?.profile?._id }));
28
+ // Deactivate chat
29
+ dispatch(chatActions.removeChatRoom({ chatRoomId }));
30
+ dispatch(chatActions.setChatUnactive());
31
+ };
32
+
33
+ return (
34
+ <Modal
35
+ onClick={() => dispatch(modalActions.closeModal())}
36
+ typeValue="deleteContactModal"
37
+ className="w-[30rem] !px-[2rem] pb-[2rem]"
38
+ canOverlayClose={true}
39
+ >
40
+ <h2 className="font-semibold text-[2rem]">Delete Contact</h2>
41
+ <p className="">
42
+ Are you sure you want to delete {contactData?.profile?.name} contact?
43
+ </p>
44
+ <div className="flex items-center justify-center gap-[2rem] mt-[1rem]">
45
+ <button className="text-cta-icon rounded-md font-semibold uppercase p-[1rem] hover:bg-secondary-light-text">
46
+ Cancel
47
+ </button>
48
+ <button
49
+ onClick={deleteContactHandler}
50
+ className="text-danger rounded-md font-semibold uppercase p-[1rem] hover:bg-secondary-light-text"
51
+ >
52
+ Delete Contact
53
+ </button>
54
+ </div>
55
+ </Modal>
56
+ );
57
+ }
58
+
59
+ export default DeleteContact;
client/src/components/globals/FormField.jsx ADDED
@@ -0,0 +1,47 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Field } from "formik";
3
+
4
+ function FormField({
5
+ labelName,
6
+ labelClassName,
7
+ name,
8
+ value,
9
+ error,
10
+ required,
11
+ fieldType,
12
+ }) {
13
+ return (
14
+ <div className="relative group">
15
+ <label
16
+ onClick={(event) =>
17
+ event.currentTarget.closest("div").querySelector("input").focus()
18
+ }
19
+ htmlFor={name}
20
+ className={`${labelClassName}
21
+ absolute
22
+ ${
23
+ value
24
+ ? "text-[1.4rem] -top-[1.1rem] -translate-y-0 "
25
+ : "top-1/2 -translate-y-1/2 text-[1.6rem]"
26
+ } group-focus-within:-top-[1.1rem]
27
+ group-focus-within:-translate-y-0
28
+ left-[1.5rem] group-focus-within:left-[1rem]
29
+ bg-primary
30
+ text-secondary-text group-focus-within:text-cta-icon
31
+ px-[.3rem]
32
+ duration-200
33
+ group-focus-within:text-[1.4rem]
34
+ `}
35
+ >
36
+ {labelName} ({required ? "required" : "optional"})
37
+ </label>
38
+ <Field
39
+ name={name}
40
+ className="bg-transparent border border-secondary-text rounded-2xl py-[1rem] group-focus-within:outline-none px-[1.5rem] focus-within:border-2 focus-within:border-cta-icon w-full"
41
+ type={fieldType ? fieldType : "text"}
42
+ />
43
+ </div>
44
+ );
45
+ }
46
+
47
+ export default FormField;
client/src/components/globals/Header.jsx ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ function Header({ children, className }) {
4
+ return <header className={`h-[5.6rem] ${className}`}>{children}</header>;
5
+ }
6
+
7
+ export default Header;
client/src/components/globals/IconWrapper.jsx ADDED
@@ -0,0 +1,16 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ function IconWrapper({ active, children, className, onClick }) {
4
+ return (
5
+ <div
6
+ className={`flex items-center justify-center w-[4rem] h-[4rem] rounded-full cursor-pointer hover:bg-secondary-light-text ${
7
+ active && "bg-secondary-light-text"
8
+ } ${className}`}
9
+ onClick={onClick}
10
+ >
11
+ {children}
12
+ </div>
13
+ );
14
+ }
15
+
16
+ export default IconWrapper;
client/src/components/globals/Image.jsx ADDED
@@ -0,0 +1,18 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { Image as CloudImage, Placeholder } from "cloudinary-react";
3
+ import process from "process";
4
+
5
+ function Image({ src, alt, className }) {
6
+ return (
7
+ <CloudImage
8
+ cloudName={process.env.REACT_APP_CLOUDINARY_NAME}
9
+ publicId={src}
10
+ alt={alt}
11
+ className={`${className} object-center object-cover`}
12
+ >
13
+ <Placeholder type="blur" />
14
+ </CloudImage>
15
+ );
16
+ }
17
+
18
+ export default Image;
client/src/components/globals/MessageCheck.jsx ADDED
@@ -0,0 +1,54 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ function MessageCheck({ readStatus, deliveredStatus, className }) {
4
+ const computedClassName = `!fill-transparent !stroke-avatar-check ${className}`;
5
+ // Message sent gives a single check
6
+ const singleCheck = (
7
+ <svg
8
+ xmlns="http://www.w3.org/2000/svg"
9
+ width="1em"
10
+ height="1em"
11
+ preserveAspectRatio="xMidYMid meet"
12
+ viewBox="0 0 24 24"
13
+ className="w-[1.4rem] h-[1.4rem]"
14
+ >
15
+ <path
16
+ fill="none"
17
+ stroke="currentColor"
18
+ strokeLinecap="round"
19
+ strokeLinejoin="round"
20
+ strokeWidth="2"
21
+ d="m4 12l6 6L20 6"
22
+ className={computedClassName}
23
+ />
24
+ </svg>
25
+ );
26
+
27
+ // Message delivered gives double check, message read gives a colored double check
28
+ const doubleCheck = (
29
+ <svg
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ width="1em"
32
+ height="1em"
33
+ preserveAspectRatio="xMidYMid meet"
34
+ viewBox="0 0 32 32"
35
+ className="w-[1.6rem] h-[1.6rem]"
36
+ >
37
+ <path
38
+ fill="none"
39
+ stroke="currentColor"
40
+ strokeLinecap="round"
41
+ strokeLinejoin="round"
42
+ strokeWidth="2"
43
+ d="m4 17l5 5l12-12m-5 10l2 2l12-12"
44
+ className={`${computedClassName} ${
45
+ readStatus && "!stroke-avatar-check-read"
46
+ }`}
47
+ />
48
+ </svg>
49
+ );
50
+
51
+ return <span>{deliveredStatus ? doubleCheck : singleCheck}</span>;
52
+ }
53
+
54
+ export default MessageCheck;
client/src/components/globals/Modal.jsx ADDED
@@ -0,0 +1,45 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { AnimatePresence, motion } from "framer-motion";
3
+ import { useSelector } from "react-redux";
4
+ import Overlay from "./Overlay";
5
+
6
+ function Modal({ className, children, typeValue, canOverlayClose, onClick }) {
7
+ const { type, positions } = useSelector((state) => state.modalReducer);
8
+
9
+ return (
10
+ <AnimatePresence>
11
+ {type === typeValue && (
12
+ <Overlay canOverlayClose={canOverlayClose === false ? false : true}>
13
+ <motion.div
14
+ initial={{
15
+ scale: 0,
16
+ }}
17
+ animate={{
18
+ scale: 1,
19
+ opacity: 1,
20
+ transition: {
21
+ duration: 0.2,
22
+ },
23
+ }}
24
+ exit={{
25
+ scale: 0,
26
+ transition: {
27
+ duration: 0.2,
28
+ },
29
+ }}
30
+ className={`bg-modal backdrop-blur-[100px] py-[1rem] px-[.5rem] flex flex-col gap-[.5rem] w-fit rounded-md shadow-md shadow-box-shadow absolute ${className}`}
31
+ id="modal"
32
+ style={{
33
+ ...positions,
34
+ }}
35
+ onClick={onClick}
36
+ >
37
+ {children}
38
+ </motion.div>
39
+ </Overlay>
40
+ )}
41
+ </AnimatePresence>
42
+ );
43
+ }
44
+
45
+ export default Modal;
client/src/components/globals/ModalChild.jsx ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ function ModalChild({ children, onClick, className }) {
4
+ return (
5
+ <div
6
+ onClick={onClick}
7
+ className={`flex items-center gap-[.8rem] hover:bg-secondary-light-text modal-child text-[1.4rem] font-medium capitalize px-[1rem] py-[.5rem] rounded-md cursor-default duration-200 active:scale-95 ${className}`}
8
+ >
9
+ {children}
10
+ </div>
11
+ );
12
+ }
13
+
14
+ export default ModalChild;
client/src/components/globals/NewContactForm.jsx ADDED
@@ -0,0 +1,130 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import Modal from "./Modal";
3
+ import { Formik, Form } from "formik";
4
+ import * as Yup from "yup";
5
+ import IconWrapper from "./IconWrapper";
6
+ import FormField from "./FormField";
7
+ import { useDispatch } from "react-redux";
8
+ import { modalActions } from "../../store/modalSlice";
9
+ import useFetch from "../../hooks/useFetch";
10
+ import { contactsActions } from "../../store/contactsSlice";
11
+ import useSocket from "../../hooks/useSocket";
12
+ import { chatListActions } from "../../store/chatListSlice";
13
+
14
+ const formSchema = Yup.object().shape({
15
+ name: Yup.string().required("Field is required"),
16
+ username: Yup.string().required("Field is required"),
17
+ });
18
+
19
+ function NewContactForm() {
20
+ const dispatch = useDispatch();
21
+ const { socketEmit } = useSocket();
22
+ const { reqFn } = useFetch({ method: "POST", url: "/contacts" }, (data) => {
23
+ dispatch(contactsActions.addContact(data.data.contact));
24
+ socketEmit("user:joinRooms", { rooms: [data.data.contact.chatRoomId] });
25
+ dispatch(
26
+ chatListActions.addToChatList({
27
+ newChat: {
28
+ chatRoomId: data.data.contact.chatRoomId,
29
+ roomType: "Private",
30
+ latestMessage: {},
31
+ unreadMessagesCount: 0,
32
+ pinned: false,
33
+ mode: null,
34
+ profile: {
35
+ name: data.data.contact.name,
36
+ ...data.data.contact.contactDetails,
37
+ },
38
+ },
39
+ })
40
+ );
41
+ });
42
+
43
+ const addContact = async (values) => {
44
+ await reqFn(values);
45
+ dispatch(modalActions.closeModal({}));
46
+ };
47
+
48
+ return (
49
+ <Modal
50
+ typeValue="newContactForm"
51
+ className="mx-[1rem] !bg-primary backdrop-blur-0"
52
+ canOverlayClose={false}
53
+ >
54
+ <Formik
55
+ validationSchema={formSchema}
56
+ initialValues={{
57
+ name: "",
58
+ username: "",
59
+ }}
60
+ onSubmit={addContact}
61
+ >
62
+ {({ errors, touched, values }) => (
63
+ <Form
64
+ className="px-[1rem] py-[1rem] flex flex-col gap-[2rem] w-[38rem]"
65
+ autoComplete="off"
66
+ >
67
+ {/* Header */}
68
+ <div className="flex items-center">
69
+ <IconWrapper onClick={() => dispatch(modalActions.closeModal())}>
70
+ <svg
71
+ xmlns="http://www.w3.org/2000/svg"
72
+ width="1em"
73
+ height="1em"
74
+ preserveAspectRatio="xMidYMid meet"
75
+ viewBox="0 0 32 32"
76
+ >
77
+ <path
78
+ fill="currentColor"
79
+ d="M24 9.4L22.6 8L16 14.6L9.4 8L8 9.4l6.6 6.6L8 22.6L9.4 24l6.6-6.6l6.6 6.6l1.4-1.4l-6.6-6.6L24 9.4z"
80
+ strokeWidth={1}
81
+ className=""
82
+ />
83
+ </svg>
84
+ </IconWrapper>
85
+ <h2 className="text-[2rem] font-semibold ml-[1rem]">
86
+ Add Contact
87
+ </h2>
88
+ <button
89
+ type="submit"
90
+ className={`text-white uppercase px-[2rem] py-[1rem] rounded-md bg-cta-icon ml-auto
91
+ ${(!values.name || !values.username) && "opacity-60"}`}
92
+ >
93
+ Add
94
+ </button>
95
+ </div>
96
+ {/* Top */}
97
+ <div className="flex items-center mb-[.5rem] gap-[1.5rem]">
98
+ {/* Avatar */}
99
+ <div className="w-[10rem] h-[10rem] rounded-full bg-cta-icon flex items-center justify-center text-[2.5rem] text-white font-bold uppercase">
100
+ {values.name[0]} {values.name?.split(" ")?.[1]?.[0]}
101
+ </div>
102
+ {/* Forms */}
103
+ <div className="flex-grow">
104
+ <FormField
105
+ name="name"
106
+ labelName="Name"
107
+ required={true}
108
+ error={errors.name}
109
+ touched={touched.name}
110
+ value={values.name}
111
+ />
112
+ </div>
113
+ </div>
114
+ {/* Bottom */}
115
+ <FormField
116
+ name="username"
117
+ labelName="Username"
118
+ required={true}
119
+ error={errors.username}
120
+ touched={touched.username}
121
+ value={values.username}
122
+ />
123
+ </Form>
124
+ )}
125
+ </Formik>
126
+ </Modal>
127
+ );
128
+ }
129
+
130
+ export default NewContactForm;
client/src/components/globals/Notification.jsx ADDED
@@ -0,0 +1,59 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useDispatch, useSelector } from "react-redux";
3
+ import { notificationActions } from "../../store/notificationSlice";
4
+ import { motion, AnimatePresence } from "framer-motion";
5
+
6
+ function Notification() {
7
+ const notifications = useSelector((state) => state.notificationReducer);
8
+ const dispatch = useDispatch();
9
+
10
+ return (
11
+ <div className="absolute right-0 z-[10000] space-y-[.5rem]">
12
+ <AnimatePresence>
13
+ {notifications.map(({ message, type, id }) => (
14
+ <motion.div
15
+ initial={{
16
+ translateX: "32rem",
17
+ opacity: 0.5,
18
+ }}
19
+ animate={{
20
+ translateX: 0,
21
+ opacity: 1,
22
+ }}
23
+ exit={{
24
+ translateX: "32rem",
25
+ opacity: 0.5,
26
+ }}
27
+ key={id}
28
+ className={`bg-primary border-l-[3px] flex justify-between pl-[2rem] w-[30rem] rounded-l-[.5rem] ${
29
+ type === "error" && "border-l-danger"
30
+ }`}
31
+ >
32
+ <span className="my-[3rem] mr-[.5rem]">{message}</span>
33
+ <span
34
+ onClick={() =>
35
+ dispatch(notificationActions.removeNotification(id))
36
+ }
37
+ className="flex items-center justify-center w-[4.5rem] hover:bg-secondary-light-text"
38
+ >
39
+ <svg
40
+ xmlns="http://www.w3.org/2000/svg"
41
+ width="1em"
42
+ height="1em"
43
+ preserveAspectRatio="xMidYMid meet"
44
+ viewBox="0 0 32 32"
45
+ >
46
+ <path
47
+ fill="currentColor"
48
+ d="M24 9.4L22.6 8L16 14.6L9.4 8L8 9.4l6.6 6.6L8 22.6L9.4 24l6.6-6.6l6.6 6.6l1.4-1.4l-6.6-6.6L24 9.4z"
49
+ />
50
+ </svg>
51
+ </span>
52
+ </motion.div>
53
+ ))}
54
+ </AnimatePresence>
55
+ </div>
56
+ );
57
+ }
58
+
59
+ export default Notification;
client/src/components/globals/Overlay.jsx ADDED
@@ -0,0 +1,27 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useDispatch } from "react-redux";
3
+ import { modalActions } from "../../store/modalSlice";
4
+
5
+ function Overlay({ children, canOverlayClose }) {
6
+ const dispatch = useDispatch();
7
+
8
+ const closeModal = () => {
9
+ if (!canOverlayClose) return;
10
+ dispatch(modalActions.closeModal());
11
+ };
12
+
13
+ return (
14
+ <div
15
+ onClick={(event) => {
16
+ if (event.target.id !== "overlay") return;
17
+ closeModal("Click caused it");
18
+ }}
19
+ className=" w-full h-full absolute z-20 top-0 left-0 flex items-center justify-center"
20
+ id="overlay"
21
+ >
22
+ {children}
23
+ </div>
24
+ );
25
+ }
26
+
27
+ export default Overlay;
client/src/components/globals/Sidebar.jsx ADDED
@@ -0,0 +1,25 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useSelector } from "react-redux";
3
+ import ChatList from "../../pages/ChatList";
4
+ import Contacts from "../../pages/Contacts";
5
+ import SelectContacts from "../../pages/SelectContacts";
6
+ import Settings from "../../pages/Settings";
7
+
8
+ function Sidebar() {
9
+ const chatActive = useSelector((state) => state.chatReducer.active);
10
+ return (
11
+ <div
12
+ id="side-bar"
13
+ className={`w-[42rem] duration-200 h-full relative overflow-x-hidden shrink-0 sm:w-full ${
14
+ chatActive && "lg:w-0"
15
+ }`}
16
+ >
17
+ <ChatList />
18
+ <Contacts />
19
+ <Settings />
20
+ <SelectContacts />
21
+ </div>
22
+ );
23
+ }
24
+
25
+ export default Sidebar;
client/src/components/globals/Spinner.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ function Spinner({ className }) {
4
+ return (
5
+ <svg
6
+ xmlns="http://www.w3.org/2000/svg"
7
+ width="1em"
8
+ height="1em"
9
+ preserveAspectRatio="xMidYMid meet"
10
+ viewBox="0 0 24 24"
11
+ className={`${className} animate-spin`}
12
+ >
13
+ <path
14
+ fill="currentColor"
15
+ d="M12 21a9 9 0 1 1 6.18-15.55a.75.75 0 0 1 0 1.06a.74.74 0 0 1-1.06 0A7.51 7.51 0 1 0 19.5 12a.75.75 0 0 1 1.5 0a9 9 0 0 1-9 9Z"
16
+ className="fill-white stroke-white"
17
+ />
18
+ </svg>
19
+ );
20
+ }
21
+
22
+ export default Spinner;
client/src/components/globals/VideoCallModal.jsx ADDED
@@ -0,0 +1,142 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import Modal from "./Modal";
3
+ import usePeer from "../../hooks/usePeer";
4
+ import { useSelector } from "react-redux";
5
+ import CTAIconWrapper from "./CTAIconWrapper";
6
+ import callReceiverTone from "../../assets/Receiver Call Request Tone.mp3";
7
+ import callSenderTone from "../../assets/Sender Call Request Tone.mp3";
8
+
9
+ function VideoCallModal() {
10
+ const { partnerProfile, callDetail } = useSelector(
11
+ (state) => state.modalReducer.payload
12
+ );
13
+
14
+ const {
15
+ userMediaRef,
16
+ callStatus,
17
+ partnerMediaRef,
18
+ acceptCall,
19
+ callAccepted,
20
+ endCall,
21
+ denyCall,
22
+ duration,
23
+ } = usePeer({
24
+ mediaOptions: { video: true, audio: true },
25
+ callDetail,
26
+ partnerProfile,
27
+ });
28
+
29
+ return (
30
+ <Modal
31
+ typeValue="videoCallModal"
32
+ canOverlayClose={false}
33
+ className="!h-full !w-full !py-0 !px-0"
34
+ >
35
+ {/* Partner video */}
36
+ <video
37
+ ref={partnerMediaRef}
38
+ className={`${
39
+ callAccepted ? "w-full h-full object-cover object-center" : "hidden"
40
+ }`}
41
+ autoPlay
42
+ playsInline
43
+ />
44
+
45
+ {/* User video */}
46
+ <video
47
+ ref={userMediaRef}
48
+ className={`rounded-lg duration-100 object-cover object-center ${
49
+ callAccepted
50
+ ? "w-[30rem] h-[30rem] absolute bottom-0 right-0 md:w-[12rem] md:h-[25rem]"
51
+ : "w-full h-full"
52
+ }`}
53
+ autoPlay
54
+ playsInline
55
+ muted
56
+ />
57
+
58
+ <div
59
+ className={`duration-100 flex items-center text-white ${
60
+ callAccepted
61
+ ? "left-[5rem] top-[3rem] gap-[3rem] absolute"
62
+ : "pt-[5rem] text-center w-full absolute flex-col gap-[1rem]"
63
+ }`}
64
+ >
65
+ <p className="text-[2.5rem] font-semibold">
66
+ {partnerProfile?.name || partnerProfile?.username}
67
+ </p>
68
+ {/* Call progress */}
69
+ <span className="capitalize">
70
+ {callAccepted ? duration : `${callStatus}...`}
71
+ </span>
72
+ </div>
73
+
74
+ {/* Pick call buttons */}
75
+ <div className="flex gap-[20rem] mt-auto mb-[5rem] absolute left-[50%] -translate-x-[50%] bottom-[5rem]">
76
+ <div
77
+ onClick={() => (callAccepted ? endCall() : denyCall("Busy"))}
78
+ className=""
79
+ >
80
+ <CTAIconWrapper className="bg-danger">
81
+ <svg
82
+ xmlns="http://www.w3.org/2000/svg"
83
+ width="1em"
84
+ height="1em"
85
+ preserveAspectRatio="xMidYMid meet"
86
+ viewBox="0 0 16 16"
87
+ >
88
+ <path
89
+ fill="currentColor"
90
+ d="M15.897 9c.125.867.207 2.053-.182 2.507c-.643.751-4.714.751-4.714-.751c0-.756.67-1.252.027-2.003c-.632-.738-1.766-.75-3.027-.751s-2.394.012-3.027.751c-.643.751.027 1.247.027 2.003c0 1.501-4.071 1.501-4.714.751C-.102 11.053-.02 9.867.105 9c.096-.579.339-1.203 1.118-2c1.168-1.09 2.935-1.98 6.716-2h.126c3.781.019 5.548.91 6.716 2c.778.797 1.022 1.421 1.118 2z"
91
+ className="fill-white stroke-transparent"
92
+ />
93
+ </svg>
94
+ </CTAIconWrapper>
95
+ </div>
96
+ {!callAccepted && !callDetail.caller && (
97
+ <div onClick={acceptCall} className="mt-auto mb-[5rem]">
98
+ <CTAIconWrapper className="bg-[#0ac630]">
99
+ <svg
100
+ xmlns="http://www.w3.org/2000/svg"
101
+ width="1em"
102
+ height="1em"
103
+ preserveAspectRatio="xMidYMid meet"
104
+ viewBox="0 0 16 16"
105
+ >
106
+ <path
107
+ fill="currentColor"
108
+ d="M15.897 9c.125.867.207 2.053-.182 2.507c-.643.751-4.714.751-4.714-.751c0-.756.67-1.252.027-2.003c-.632-.738-1.766-.75-3.027-.751s-2.394.012-3.027.751c-.643.751.027 1.247.027 2.003c0 1.501-4.071 1.501-4.714.751C-.102 11.053-.02 9.867.105 9c.096-.579.339-1.203 1.118-2c1.168-1.09 2.935-1.98 6.716-2h.126c3.781.019 5.548.91 6.716 2c.778.797 1.022 1.421 1.118 2z"
109
+ className="fill-white stroke-transparent"
110
+ />
111
+ </svg>
112
+ </CTAIconWrapper>
113
+ </div>
114
+ )}
115
+ </div>
116
+
117
+ {/* Ringtones */}
118
+ {callDetail.caller && !callAccepted && (
119
+ <audio
120
+ src={callSenderTone}
121
+ autoPlay
122
+ playsInline
123
+ loop
124
+ hidden
125
+ controlsList="nodownload"
126
+ />
127
+ )}
128
+ {!callDetail.caller && !callAccepted && (
129
+ <audio
130
+ src={callReceiverTone}
131
+ autoPlay
132
+ playsInline
133
+ loop
134
+ hidden
135
+ controlsList="nodownload"
136
+ />
137
+ )}
138
+ </Modal>
139
+ );
140
+ }
141
+
142
+ export default VideoCallModal;
client/src/components/globals/VoiceCallModal.jsx ADDED
@@ -0,0 +1,137 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useSelector } from "react-redux";
3
+ import CTAIconWrapper from "./CTAIconWrapper";
4
+ import Modal from "./Modal";
5
+ import Image from "../globals/Image";
6
+ import usePeer from "../../hooks/usePeer";
7
+ import callReceiverTone from "../../assets/Receiver Call Request Tone.mp3";
8
+ import callSenderTone from "../../assets/Sender Call Request Tone.mp3";
9
+
10
+ function VoiceCallModal() {
11
+ const { partnerProfile, callDetail } = useSelector(
12
+ (state) => state.modalReducer.payload
13
+ );
14
+
15
+ const {
16
+ userMediaRef,
17
+ callStatus,
18
+ partnerMediaRef,
19
+ acceptCall,
20
+ callAccepted,
21
+ endCall,
22
+ denyCall,
23
+ duration,
24
+ } = usePeer({
25
+ mediaOptions: { video: false, audio: true },
26
+ callDetail,
27
+ partnerProfile,
28
+ });
29
+
30
+ return (
31
+ <Modal
32
+ typeValue="voiceCallModal"
33
+ canOverlayClose={false}
34
+ className="!p-0 !rounded-3xl sm:!rounded-none overflow-hidden text-white h-fit"
35
+ >
36
+ <Image
37
+ src={partnerProfile?.avatar}
38
+ alt={partnerProfile?.username}
39
+ className="w-[40rem] h-[50rem] sm:w-full sm:h-full"
40
+ />
41
+ <div
42
+ className={`flex flex-col items-center absolute top-0 right-0 w-full h-full`}
43
+ >
44
+ <div className="pt-[5rem] text-center w-full">
45
+ <p className="text-[2.5rem] font-semibold">
46
+ {partnerProfile?.name || partnerProfile?.username}
47
+ </p>
48
+ {/* Call progress */}
49
+ <span className="block mt-[1rem] capitalize">
50
+ {callAccepted ? duration : `${callStatus}...`}
51
+ </span>
52
+ </div>
53
+ <div className="flex gap-[20rem] mt-auto mb-[5rem]">
54
+ <div
55
+ onClick={() => (callAccepted ? endCall() : denyCall("Busy"))}
56
+ className=""
57
+ >
58
+ <CTAIconWrapper className="bg-danger">
59
+ <svg
60
+ xmlns="http://www.w3.org/2000/svg"
61
+ width="1em"
62
+ height="1em"
63
+ preserveAspectRatio="xMidYMid meet"
64
+ viewBox="0 0 16 16"
65
+ >
66
+ <path
67
+ fill="currentColor"
68
+ d="M15.897 9c.125.867.207 2.053-.182 2.507c-.643.751-4.714.751-4.714-.751c0-.756.67-1.252.027-2.003c-.632-.738-1.766-.75-3.027-.751s-2.394.012-3.027.751c-.643.751.027 1.247.027 2.003c0 1.501-4.071 1.501-4.714.751C-.102 11.053-.02 9.867.105 9c.096-.579.339-1.203 1.118-2c1.168-1.09 2.935-1.98 6.716-2h.126c3.781.019 5.548.91 6.716 2c.778.797 1.022 1.421 1.118 2z"
69
+ className="fill-white stroke-transparent"
70
+ />
71
+ </svg>
72
+ </CTAIconWrapper>
73
+ </div>
74
+ {!callAccepted && !callDetail.caller && (
75
+ <div onClick={acceptCall} className="mt-auto mb-[5rem]">
76
+ <CTAIconWrapper className="bg-[#0ac630]">
77
+ <svg
78
+ xmlns="http://www.w3.org/2000/svg"
79
+ width="1em"
80
+ height="1em"
81
+ preserveAspectRatio="xMidYMid meet"
82
+ viewBox="0 0 16 16"
83
+ >
84
+ <path
85
+ fill="currentColor"
86
+ d="M15.897 9c.125.867.207 2.053-.182 2.507c-.643.751-4.714.751-4.714-.751c0-.756.67-1.252.027-2.003c-.632-.738-1.766-.75-3.027-.751s-2.394.012-3.027.751c-.643.751.027 1.247.027 2.003c0 1.501-4.071 1.501-4.714.751C-.102 11.053-.02 9.867.105 9c.096-.579.339-1.203 1.118-2c1.168-1.09 2.935-1.98 6.716-2h.126c3.781.019 5.548.91 6.716 2c.778.797 1.022 1.421 1.118 2z"
87
+ className="fill-white stroke-transparent"
88
+ />
89
+ </svg>
90
+ </CTAIconWrapper>
91
+ </div>
92
+ )}
93
+ </div>
94
+ </div>
95
+ {/* Speakers */}
96
+
97
+ <audio
98
+ ref={userMediaRef}
99
+ autoPlay
100
+ playsInline
101
+ muted
102
+ hidden
103
+ controlsList="nodownload"
104
+ />
105
+ <audio
106
+ ref={partnerMediaRef}
107
+ autoPlay
108
+ playsInline
109
+ hidden
110
+ controlsList="nodownload"
111
+ />
112
+ {/* Ringtones */}
113
+ {callDetail.caller && !callAccepted && (
114
+ <audio
115
+ src={callSenderTone}
116
+ autoPlay
117
+ playsInline
118
+ loop
119
+ hidden
120
+ controlsList="nodownload"
121
+ />
122
+ )}
123
+ {!callDetail.caller && !callAccepted && (
124
+ <audio
125
+ src={callReceiverTone}
126
+ autoPlay
127
+ playsInline
128
+ loop
129
+ hidden
130
+ controlsList="nodownload"
131
+ />
132
+ )}
133
+ </Modal>
134
+ );
135
+ }
136
+
137
+ export default VoiceCallModal;
client/src/components/pages/Authentication/Login.jsx ADDED
@@ -0,0 +1,87 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import * as Yup from "yup";
3
+ import { Formik, Form } from "formik";
4
+ import FormField from "../../globals/FormField";
5
+ import useFetch from "../../../hooks/useFetch";
6
+ import { useDispatch } from "react-redux";
7
+ import { authActions } from "../../../store/authSlice";
8
+ import Spinner from "../../globals/Spinner";
9
+
10
+ const schema = Yup.object().shape({
11
+ username: Yup.string().required("Field is required"),
12
+ password: Yup.string().required("Field is required"),
13
+ });
14
+
15
+ function Login({ setUserWantsToLogin }) {
16
+ const dispatch = useDispatch();
17
+ // Request to log user in
18
+ const { reqState, reqFn: loginRequest } = useFetch(
19
+ { url: "/user/login", method: "POST" },
20
+ // Success
21
+ () => {
22
+ dispatch(authActions.login());
23
+ }
24
+ );
25
+
26
+ return (
27
+ <div className="basis-[35rem]">
28
+ <h1 className="text-cta-icon font-semibold text-[2rem] uppercase mb-[2rem]">
29
+ Login To Telegram
30
+ </h1>
31
+ <Formik
32
+ initialValues={{
33
+ username: "",
34
+ password: "",
35
+ }}
36
+ validationSchema={schema}
37
+ onSubmit={loginRequest}
38
+ >
39
+ {({ errors, values }) => (
40
+ <Form className="flex flex-col gap-[1.5rem]" autoComplete="off">
41
+ <FormField
42
+ labelName="Username"
43
+ labelClassName={`bg-transparent group-focus-within:hidden ${
44
+ values.username && "hidden"
45
+ }`}
46
+ name="username"
47
+ required={true}
48
+ value={values.username}
49
+ error={errors.username}
50
+ />
51
+
52
+ <FormField
53
+ labelName="Password"
54
+ labelClassName={`bg-transparent group-focus-within:hidden ${
55
+ values.password && "hidden"
56
+ }`}
57
+ name="password"
58
+ required={true}
59
+ value={values.password}
60
+ error={errors.password}
61
+ fieldType="password"
62
+ />
63
+ <button
64
+ className={`bg-cta-icon mt-[1rem] p-[1rem] rounded-xl uppercase text-white font-semibold opacity-80 flex items-center justify-center ${
65
+ !errors.username && !errors.password && "opacity-100"
66
+ }`}
67
+ type="submit"
68
+ >
69
+ {reqState !== "loading" && "Login"}
70
+ {reqState === "loading" && (
71
+ <Spinner className="w-[2.5rem] h-[2.5rem]" />
72
+ )}
73
+ </button>
74
+ </Form>
75
+ )}
76
+ </Formik>
77
+ <div
78
+ onClick={() => setUserWantsToLogin(false)}
79
+ className="mt-[2rem] text-right text-secondary-text underline cursor-pointer hover:text-cta-icon"
80
+ >
81
+ New to Telegram
82
+ </div>
83
+ </div>
84
+ );
85
+ }
86
+
87
+ export default Login;
client/src/components/pages/Authentication/Register.jsx ADDED
@@ -0,0 +1,146 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import * as Yup from "yup";
3
+ import { Formik, Form } from "formik";
4
+ import FormField from "../../globals/FormField";
5
+ import useFetch from "../../../hooks/useFetch";
6
+ import { useDispatch } from "react-redux";
7
+ import { authActions } from "../../../store/authSlice";
8
+ import Spinner from "../../globals/Spinner";
9
+ import useChatBot from "../../../hooks/useChatBot";
10
+
11
+ const schema = Yup.object().shape({
12
+ name: Yup.string()
13
+ .min(1, "Too short")
14
+ .max(50, "Too long")
15
+ .required("Field is required"),
16
+ username: Yup.string()
17
+ .min(1, "Too short")
18
+ .max(50, "Too long")
19
+ .required("Field is required"),
20
+ password: Yup.string().required("Field is required"),
21
+ confirmPassword: Yup.string()
22
+ .required("Field is required")
23
+ .oneOf([Yup.ref("password"), null], "Passwords must match"),
24
+ });
25
+
26
+ function Register({ setUserWantsToLogin }) {
27
+ const dispatch = useDispatch();
28
+
29
+ // Add bot as contact
30
+ const { reqFn: addBotAsContact, reqState: addBotState } = useFetch(
31
+ { url: "/contacts", method: "POST" },
32
+ (data) => {
33
+ dispatch(
34
+ authActions.setUserIsNew({
35
+ isNew: true,
36
+ payload: { chatRoomId: data.data.contact.chatRoomId },
37
+ })
38
+ );
39
+
40
+ dispatch(authActions.login());
41
+ }
42
+ );
43
+
44
+ // Register user
45
+ const { reqFn, reqState } = useFetch(
46
+ { url: "/user/register", method: "POST" },
47
+ () => {
48
+ addBotAsContact({
49
+ name: process.env.REACT_APP_BOT_NAME,
50
+ username: process.env.REACT_APP_BOT_USERNAME,
51
+ });
52
+ }
53
+ );
54
+
55
+ return (
56
+ <div className="basis-[35rem]">
57
+ <h1 className="text-cta-icon font-semibold text-[2rem] uppercase mb-[2rem]">
58
+ Join Telegram
59
+ </h1>
60
+ <Formik
61
+ initialValues={{
62
+ name: "",
63
+ username: "",
64
+ password: "",
65
+ confirmPassword: "",
66
+ }}
67
+ validationSchema={schema}
68
+ onSubmit={reqFn}
69
+ >
70
+ {({ errors, values }) => (
71
+ <Form className="flex flex-col gap-[1.5rem]" autoComplete="off">
72
+ <FormField
73
+ labelName="Name"
74
+ labelClassName={`bg-transparent group-focus-within:hidden ${
75
+ values.name && "hidden"
76
+ }`}
77
+ name="name"
78
+ required={true}
79
+ value={values.name}
80
+ error={errors.name}
81
+ />
82
+
83
+ <FormField
84
+ labelName="Username"
85
+ labelClassName={`bg-transparent group-focus-within:hidden ${
86
+ values.username && "hidden"
87
+ }`}
88
+ name="username"
89
+ required={true}
90
+ value={values.username}
91
+ error={errors.username}
92
+ />
93
+
94
+ <FormField
95
+ labelName="Password"
96
+ labelClassName={`bg-transparent group-focus-within:hidden ${
97
+ values.password && "hidden"
98
+ }`}
99
+ name="password"
100
+ required={true}
101
+ value={values.password}
102
+ error={errors.password}
103
+ fieldType="password"
104
+ />
105
+
106
+ <FormField
107
+ labelName="Confirm Password"
108
+ labelClassName={`bg-transparent group-focus-within:hidden ${
109
+ values.confirmPassword && "hidden"
110
+ }`}
111
+ name="confirmPassword"
112
+ required={true}
113
+ value={values.confirmPassword}
114
+ error={errors.confirmPassword}
115
+ fieldType="password"
116
+ />
117
+ <button
118
+ className={`bg-cta-icon mt-[1rem] p-[1rem] rounded-xl uppercase text-white font-semibold opacity-80 flex items-center justify-center ${
119
+ !errors.name &&
120
+ !errors.username &&
121
+ !errors.phoneNumber &&
122
+ !errors.password &&
123
+ !errors.confirmPassword &&
124
+ "opacity-100"
125
+ }`}
126
+ type="submit"
127
+ >
128
+ {reqState !== "loading" && addBotState !== "loading" && "Join"}
129
+ {(reqState === "loading" || addBotState === "loading") && (
130
+ <Spinner className="w-[2.5rem] h-[2.5rem]" />
131
+ )}
132
+ </button>
133
+ </Form>
134
+ )}
135
+ </Formik>
136
+ <div
137
+ onClick={() => setUserWantsToLogin(true)}
138
+ className="mt-[2rem] text-right text-secondary-text underline cursor-pointer hover:text-cta-icon"
139
+ >
140
+ Already on Telegram
141
+ </div>
142
+ </div>
143
+ );
144
+ }
145
+
146
+ export default Register;
client/src/components/pages/Chat/ActionsModal.jsx ADDED
@@ -0,0 +1,106 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useDispatch, useSelector } from "react-redux";
3
+ import { modalActions } from "../../../store/modalSlice";
4
+ import Modal from "../../globals/Modal";
5
+ import ModalChild from "../../globals/ModalChild";
6
+
7
+ function ActionsModal({ chatProfile }) {
8
+ const dispatch = useDispatch();
9
+ const chatRoom = useSelector((state) => state.chatReducer.currentChatRoom);
10
+ return (
11
+ <Modal typeValue="actionsModal" className="origin-top-right !w-[18rem]">
12
+ <ModalChild
13
+ onClick={() => {
14
+ dispatch(
15
+ modalActions.openModal({
16
+ type: "voiceCallModal",
17
+ modalChange: true,
18
+ payload: {
19
+ partnerProfile: chatProfile,
20
+ callDetail: { caller: true },
21
+ },
22
+ positions: {},
23
+ })
24
+ );
25
+ }}
26
+ >
27
+ <svg
28
+ xmlns="http://www.w3.org/2000/svg"
29
+ width="1em"
30
+ height="1em"
31
+ preserveAspectRatio="xMidYMid meet"
32
+ viewBox="0 0 32 32"
33
+ >
34
+ <path
35
+ fill="currentColor"
36
+ d="M26 29h-.17C6.18 27.87 3.39 11.29 3 6.23A3 3 0 0 1 5.76 3h5.51a2 2 0 0 1 1.86 1.26L14.65 8a2 2 0 0 1-.44 2.16l-2.13 2.15a9.37 9.37 0 0 0 7.58 7.6l2.17-2.15a2 2 0 0 1 2.17-.41l3.77 1.51A2 2 0 0 1 29 20.72V26a3 3 0 0 1-3 3ZM6 5a1 1 0 0 0-1 1v.08C5.46 12 8.41 26 25.94 27a1 1 0 0 0 1.06-.94v-5.34l-3.77-1.51l-2.87 2.85l-.48-.06c-8.7-1.09-9.88-9.79-9.88-9.88l-.06-.48l2.84-2.87L11.28 5Z"
37
+ className="!stroke-transparent"
38
+ />
39
+ </svg>
40
+ Call
41
+ </ModalChild>
42
+ <ModalChild
43
+ onClick={() => {
44
+ dispatch(
45
+ modalActions.openModal({
46
+ type: "videoCallModal",
47
+ payload: {
48
+ partnerProfile: chatProfile,
49
+ callDetail: { caller: true },
50
+ },
51
+ positions: {},
52
+ })
53
+ );
54
+ }}
55
+ >
56
+ <svg
57
+ xmlns="http://www.w3.org/2000/svg"
58
+ width="1em"
59
+ height="1em"
60
+ preserveAspectRatio="xMidYMid meet"
61
+ viewBox="0 0 32 32"
62
+ >
63
+ <path
64
+ fill="currentColor"
65
+ d="M2 8v16h22v-3.375l4.563 2.28l1.437.72V8.375l-1.438.72L24 11.374V8H2zm2 2h18v12H4V10zm24 1.625v8.75l-4-2v-4.75l4-2z"
66
+ className="!stroke-transparent"
67
+ />
68
+ </svg>
69
+ Video Call
70
+ </ModalChild>
71
+ <ModalChild
72
+ onClick={() => {
73
+ dispatch(
74
+ modalActions.openModal({
75
+ type: chatRoom.roomType ? "deleteChatModal" : "leaveGroupModal",
76
+ payload: { chatData: chatRoom },
77
+ positions: {},
78
+ })
79
+ );
80
+ }}
81
+ className="text-danger"
82
+ >
83
+ <svg
84
+ xmlns="http://www.w3.org/2000/svg"
85
+ width="1em"
86
+ height="1em"
87
+ preserveAspectRatio="xMidYMid meet"
88
+ viewBox="0 0 24 24"
89
+ >
90
+ <path
91
+ fill="none"
92
+ stroke="currentColor"
93
+ strokeLinecap="round"
94
+ strokeLinejoin="round"
95
+ strokeWidth="2"
96
+ d="M9 7v0a3 3 0 0 1 3-3v0a3 3 0 0 1 3 3v0M9 7h6M9 7H6m9 0h3m2 0h-2M4 7h2m0 0v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7"
97
+ className="!fill-transparent !stroke-danger"
98
+ />
99
+ </svg>
100
+ {chatRoom.roomType === "Private" ? "Delete Chat" : "Leave Group"}
101
+ </ModalChild>
102
+ </Modal>
103
+ );
104
+ }
105
+
106
+ export default ActionsModal;
client/src/components/pages/Chat/AttachFileModal.jsx ADDED
@@ -0,0 +1,52 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import Modal from "../../globals/Modal";
3
+ import ModalChild from "../../globals/ModalChild";
4
+ import useUpload from "../../../hooks/useUpload";
5
+ import useSendMessage from "../../../hooks/useSendMessage";
6
+
7
+ function AttachFileModal() {
8
+ const { sendMessage } = useSendMessage();
9
+ const { handleFileUpload } = useUpload(
10
+ (uploadData) => {
11
+ sendMessage({ url: uploadData.public_id });
12
+ },
13
+ ["Image"]
14
+ );
15
+ return (
16
+ <Modal typeValue="attachFileModal">
17
+ <ModalChild
18
+ onClick={(event) =>
19
+ event.currentTarget.querySelector("#attachFile").click()
20
+ }
21
+ >
22
+ <input
23
+ type="file"
24
+ name="attachFile"
25
+ id="attachFile"
26
+ hidden
27
+ onChange={handleFileUpload}
28
+ />
29
+ <svg
30
+ xmlns="http://www.w3.org/2000/svg"
31
+ width="1em"
32
+ height="1em"
33
+ preserveAspectRatio="xMidYMid meet"
34
+ viewBox="0 0 24 24"
35
+ >
36
+ <path
37
+ fill="none"
38
+ stroke="currentColor"
39
+ strokeLinecap="round"
40
+ strokeLinejoin="round"
41
+ strokeWidth="1.5"
42
+ d="m2.25 15.75l5.159-5.159a2.25 2.25 0 0 1 3.182 0l5.159 5.159m-1.5-1.5l1.409-1.409a2.25 2.25 0 0 1 3.182 0l2.909 2.909m-18 3.75h16.5a1.5 1.5 0 0 0 1.5-1.5V6a1.5 1.5 0 0 0-1.5-1.5H3.75A1.5 1.5 0 0 0 2.25 6v12a1.5 1.5 0 0 0 1.5 1.5Zm10.5-11.25h.008v.008h-.008V8.25Zm.375 0a.375.375 0 1 1-.75 0a.375.375 0 0 1 .75 0Z"
43
+ className="!fill-transparent"
44
+ />
45
+ </svg>
46
+ Photo
47
+ </ModalChild>
48
+ </Modal>
49
+ );
50
+ }
51
+
52
+ export default AttachFileModal;
client/src/components/pages/Chat/AttachFileOrRecordDuration.jsx ADDED
@@ -0,0 +1,53 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useDispatch } from "react-redux";
3
+ import { modalActions } from "../../../store/modalSlice";
4
+ import IconWrapper from "../../globals/IconWrapper";
5
+
6
+ function AttachFileOrRecordDuration({ isRecording, formattedTime }) {
7
+ const dispatch = useDispatch();
8
+ // If user is not recording, show them option to attach an image or video
9
+ if (!isRecording)
10
+ return (
11
+ <IconWrapper
12
+ className="shrink-0"
13
+ onClick={() =>
14
+ dispatch(
15
+ modalActions.openModal({
16
+ type: "attachFileModal",
17
+ positions: { bottom: 90, right: 80 },
18
+ })
19
+ )
20
+ }
21
+ >
22
+ <svg
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ width="1em"
25
+ height="1em"
26
+ preserveAspectRatio="xMidYMid meet"
27
+ viewBox="0 0 24 24"
28
+ >
29
+ <path
30
+ fill="none"
31
+ stroke="currentColor"
32
+ strokeLinecap="round"
33
+ strokeLinejoin="round"
34
+ strokeWidth="2"
35
+ d="m15.172 7l-6.586 6.586a2 2 0 1 0 2.828 2.828l6.414-6.586a4 4 0 0 0-5.656-5.656l-6.415 6.585a6 6 0 1 0 8.486 8.486L20.5 13"
36
+ className="fill-transparent"
37
+ />
38
+ </svg>
39
+ </IconWrapper>
40
+ );
41
+
42
+ if (isRecording)
43
+ return (
44
+ <div className="self-center flex items-center gap-[1rem]">
45
+ <span>{formattedTime}</span>
46
+ <span className="w-[1rem] h-[1rem] bg-danger rounded-full animate-pulse">
47
+ &nbsp;
48
+ </span>
49
+ </div>
50
+ );
51
+ }
52
+
53
+ export default AttachFileOrRecordDuration;
client/src/components/pages/Chat/BubbleTail.jsx ADDED
@@ -0,0 +1,22 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+
3
+ function BubbleTail({ className, fillColor }) {
4
+ return (
5
+ <svg
6
+ width="11"
7
+ height="10"
8
+ viewBox="0 0 11 20"
9
+ fill="none"
10
+ xmlns="http://www.w3.org/2000/svg"
11
+ className={`${className} w-[2.5rem] h-[2.5rem] translate-x-[-.6rem] shrink-0`}
12
+ >
13
+ <path
14
+ d="M11 20C4.46592 14.9222 2.16956 10.4109 0 0V20H11Z"
15
+ fill="#8774E1"
16
+ className={`${fillColor}`}
17
+ />
18
+ </svg>
19
+ );
20
+ }
21
+
22
+ export default BubbleTail;
client/src/components/pages/Chat/CTAButtons.jsx ADDED
@@ -0,0 +1,194 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import { AnimatePresence, motion } from "framer-motion";
2
+ import React from "react";
3
+ import { useDispatch, useSelector } from "react-redux";
4
+ import useSendMessage from "../../../hooks/useSendMessage";
5
+ import { modalActions } from "../../../store/modalSlice";
6
+ import CTAIconWrapper from "../../globals/CTAIconWrapper";
7
+
8
+ function CTAButtons({
9
+ isTyping,
10
+ isRecording,
11
+ endRecording,
12
+ startRecording,
13
+ setMessageEmpty,
14
+ pauseRecording,
15
+ }) {
16
+ const { sendMessage } = useSendMessage(setMessageEmpty);
17
+ const dispatch = useDispatch();
18
+
19
+ const isSending = useSelector(
20
+ (state) =>
21
+ state.chatReducer.mode === "sending" ||
22
+ state.chatReducer.mode?.endsWith("Upload")
23
+ );
24
+
25
+ return (
26
+ <div className="shrink-0 relative">
27
+ {/* Pause recording */}
28
+ {isRecording && (
29
+ <CTAIconWrapper
30
+ className="absolute bg-danger top-0 -left-[7rem] z-10"
31
+ onClick={() => {
32
+ dispatch(
33
+ modalActions.openModal({ type: "stopRecordModal", positions: {} })
34
+ );
35
+ pauseRecording();
36
+ }}
37
+ >
38
+ <svg
39
+ xmlns="http://www.w3.org/2000/svg"
40
+ width="1em"
41
+ height="1em"
42
+ preserveAspectRatio="xMidYMid meet"
43
+ viewBox="0 0 24 24"
44
+ >
45
+ <path
46
+ fill="none"
47
+ stroke="currentColor"
48
+ strokeLinecap="round"
49
+ strokeLinejoin="round"
50
+ strokeWidth="2"
51
+ d="M9 7v0a3 3 0 0 1 3-3v0a3 3 0 0 1 3 3v0M9 7h6M9 7H6m9 0h3m2 0h-2M4 7h2m0 0v11a2 2 0 0 0 2 2h8a2 2 0 0 0 2-2V7"
52
+ className="!fill-transparent !stroke-white"
53
+ />
54
+ </svg>
55
+ </CTAIconWrapper>
56
+ )}
57
+
58
+ {/* Start recording or send message*/}
59
+ <CTAIconWrapper
60
+ onClick={() => {
61
+ if (!isTyping && !isRecording) startRecording();
62
+ else {
63
+ if (isRecording) {
64
+ endRecording();
65
+ return;
66
+ }
67
+
68
+ sendMessage();
69
+ }
70
+ }}
71
+ className={`relative ${isRecording && "animate-wave"}`}
72
+ >
73
+ {/* Sending message Icon */}
74
+ <AnimatePresence>
75
+ {isSending && (
76
+ <motion.svg
77
+ xmlns="http://www.w3.org/2000/svg"
78
+ width="1em"
79
+ height="1em"
80
+ viewBox="0 0 24 24"
81
+ >
82
+ <circle
83
+ cx="4"
84
+ cy="12"
85
+ r="3"
86
+ fill="currentColor"
87
+ className="fill-white stroke-transparent"
88
+ >
89
+ <animate
90
+ id="svgSpinners3DotsBounce0"
91
+ attributeName="cy"
92
+ begin="0;svgSpinners3DotsBounce1.end+0.25s"
93
+ calcMode="spline"
94
+ dur="0.6s"
95
+ keySplines=".33,.66,.66,1;.33,0,.66,.33"
96
+ values="12;6;12"
97
+ />
98
+ </circle>
99
+ <circle
100
+ cx="12"
101
+ cy="12"
102
+ r="3"
103
+ fill="currentColor"
104
+ className="fill-white stroke-transparent"
105
+ >
106
+ <animate
107
+ attributeName="cy"
108
+ begin="svgSpinners3DotsBounce0.begin+0.1s"
109
+ calcMode="spline"
110
+ dur="0.6s"
111
+ keySplines=".33,.66,.66,1;.33,0,.66,.33"
112
+ values="12;6;12"
113
+ />
114
+ </circle>
115
+ <circle
116
+ cx="20"
117
+ cy="12"
118
+ r="3"
119
+ fill="currentColor"
120
+ className="fill-white stroke-transparent"
121
+ >
122
+ <animate
123
+ id="svgSpinners3DotsBounce1"
124
+ attributeName="cy"
125
+ begin="svgSpinners3DotsBounce0.begin+0.2s"
126
+ calcMode="spline"
127
+ dur="0.6s"
128
+ keySplines=".33,.66,.66,1;.33,0,.66,.33"
129
+ values="12;6;12"
130
+ />
131
+ </circle>
132
+ </motion.svg>
133
+ )}
134
+ </AnimatePresence>
135
+
136
+ {/* Microphone */}
137
+ <AnimatePresence>
138
+ {!isTyping && !isRecording && !isSending && (
139
+ <motion.svg
140
+ initial={{ scale: 0 }}
141
+ animate={{ scale: 1 }}
142
+ exit={{ scale: 0 }}
143
+ xmlns="http://www.w3.org/2000/svg"
144
+ width="1em"
145
+ height="1em"
146
+ preserveAspectRatio="xMidYMid meet"
147
+ viewBox="0 0 32 32"
148
+ >
149
+ <path
150
+ fill="currentColor"
151
+ d="M23 14v3a7 7 0 0 1-14 0v-3H7v3a9 9 0 0 0 8 8.94V28h-4v2h10v-2h-4v-2.06A9 9 0 0 0 25 17v-3Z"
152
+ className="fill-white stroke-transparent"
153
+ />
154
+ <path
155
+ fill="currentColor"
156
+ d="M16 22a5 5 0 0 0 5-5V7a5 5 0 0 0-10 0v10a5 5 0 0 0 5 5Z"
157
+ className="fill-white stroke-transparent"
158
+ />
159
+ </motion.svg>
160
+ )}
161
+ </AnimatePresence>
162
+
163
+ {/* Send icon */}
164
+ <AnimatePresence>
165
+ {(isTyping || isRecording) && (
166
+ <motion.svg
167
+ initial={{ scale: 0 }}
168
+ animate={{ scale: 1 }}
169
+ exit={{ scale: 0 }}
170
+ xmlns="http://www.w3.org/2000/svg"
171
+ width="1em"
172
+ height="1em"
173
+ preserveAspectRatio="xMidYMid meet"
174
+ viewBox="0 0 24 24"
175
+ className="absolute"
176
+ >
177
+ <path
178
+ fill="none"
179
+ stroke="currentColor"
180
+ strokeLinecap="round"
181
+ strokeLinejoin="round"
182
+ strokeWidth="2"
183
+ d="M9.912 12H4L2.023 4.135A.662.662 0 0 1 2 3.995c-.022-.721.772-1.221 1.46-.891L22 12L3.46 20.896c-.68.327-1.464-.159-1.46-.867a.66.66 0 0 1 .033-.186L3.5 15"
184
+ className="fill-white stroke-white"
185
+ />
186
+ </motion.svg>
187
+ )}
188
+ </AnimatePresence>
189
+ </CTAIconWrapper>
190
+ </div>
191
+ );
192
+ }
193
+
194
+ export default CTAButtons;
client/src/components/pages/Chat/CallMessage.jsx ADDED
@@ -0,0 +1,120 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import MessageReadStatus from "./MessageReadStatus";
3
+
4
+ function CallMessage({
5
+ callDetails,
6
+ deliveredStatus,
7
+ readStatus,
8
+ time,
9
+ messageReceived,
10
+ }) {
11
+ return (
12
+ <div
13
+ className={`flex rounded-3xl ${
14
+ messageReceived
15
+ ? "rounded-bl-none bg-primary"
16
+ : "rounded-br-none bg-message"
17
+ } p-[1.5rem] gap-[1rem]`}
18
+ >
19
+ <div className="flex items-center gap-[1rem]">
20
+ {/* Phone Icon */}
21
+ {callDetails.callType === "voice" && (
22
+ <svg
23
+ xmlns="http://www.w3.org/2000/svg"
24
+ width="1em"
25
+ height="1em"
26
+ preserveAspectRatio="xMidYMid meet"
27
+ viewBox="0 0 32 32"
28
+ >
29
+ <path
30
+ fill="currentColor"
31
+ d="M26 29h-.17C6.18 27.87 3.39 11.29 3 6.23A3 3 0 0 1 5.76 3h5.51a2 2 0 0 1 1.86 1.26L14.65 8a2 2 0 0 1-.44 2.16l-2.13 2.15a9.37 9.37 0 0 0 7.58 7.6l2.17-2.15a2 2 0 0 1 2.17-.41l3.77 1.51A2 2 0 0 1 29 20.72V26a3 3 0 0 1-3 3ZM6 5a1 1 0 0 0-1 1v.08C5.46 12 8.41 26 25.94 27a1 1 0 0 0 1.06-.94v-5.34l-3.77-1.51l-2.87 2.85l-.48-.06c-8.7-1.09-9.88-9.79-9.88-9.88l-.06-.48l2.84-2.87L11.28 5Z"
32
+ className="!stroke-transparent fill-primary-text"
33
+ />
34
+ </svg>
35
+ )}
36
+
37
+ {/* Video Icon */}
38
+ {callDetails.callType === "video" && (
39
+ <svg
40
+ xmlns="http://www.w3.org/2000/svg"
41
+ width="1em"
42
+ height="1em"
43
+ preserveAspectRatio="xMidYMid meet"
44
+ viewBox="0 0 32 32"
45
+ >
46
+ <path
47
+ fill="currentColor"
48
+ d="M2 8v16h22v-3.375l4.563 2.28l1.437.72V8.375l-1.438.72L24 11.374V8H2zm2 2h18v12H4V10zm24 1.625v8.75l-4-2v-4.75l4-2z"
49
+ className="!stroke-transparent fill-primary-text"
50
+ />
51
+ </svg>
52
+ )}
53
+ <div className="flex flex-col">
54
+ <p className="font-semibold">
55
+ {messageReceived ? "Incoming call" : "Outgoing call"}
56
+ </p>
57
+ <div className="flex items-center gap-[.5rem]">
58
+ {messageReceived ? (
59
+ <svg
60
+ xmlns="http://www.w3.org/2000/svg"
61
+ width="1em"
62
+ height="1em"
63
+ preserveAspectRatio="xMidYMid meet"
64
+ viewBox="0 0 24 24"
65
+ className="w-[2rem] h-[2rem]"
66
+ >
67
+ <path
68
+ fill="none"
69
+ stroke="currentColor"
70
+ strokeLinecap="round"
71
+ strokeLinejoin="round"
72
+ strokeWidth="2"
73
+ d="M6 18L18 6M6 8v10h10"
74
+ className={`fill-transparent stroke-avatar-check ${
75
+ callDetails.callRejectReason && "!stroke-danger"
76
+ }`}
77
+ />
78
+ </svg>
79
+ ) : (
80
+ <svg
81
+ xmlns="http://www.w3.org/2000/svg"
82
+ width="1em"
83
+ height="1em"
84
+ preserveAspectRatio="xMidYMid meet"
85
+ viewBox="0 0 24 24"
86
+ className="w-[2rem] h-[2rem]"
87
+ >
88
+ <path
89
+ fill="none"
90
+ stroke="currentColor"
91
+ strokeLinecap="round"
92
+ strokeLinejoin="round"
93
+ strokeWidth="2"
94
+ d="M18 6L6 18M8 6h10v10"
95
+ className={`fill-transparent stroke-avatar-check ${
96
+ callDetails.callRejectReason && "!stroke-danger"
97
+ }`}
98
+ />
99
+ </svg>
100
+ )}
101
+ <p className={`text-[1.4rem]`}>
102
+ {callDetails.callDuration
103
+ ? callDetails.callDuration
104
+ : callDetails.callRejectReason}
105
+ </p>
106
+ </div>
107
+ </div>
108
+ </div>
109
+ <MessageReadStatus
110
+ readStatus={readStatus}
111
+ deliveredStatus={deliveredStatus}
112
+ messageReceived={messageReceived}
113
+ time={time}
114
+ className="self-end translate-y-[1rem]"
115
+ />
116
+ </div>
117
+ );
118
+ }
119
+
120
+ export default CallMessage;
client/src/components/pages/Chat/ChatHeader.jsx ADDED
@@ -0,0 +1,119 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useDispatch, useSelector } from "react-redux";
3
+ import useChat from "../../../hooks/useChat";
4
+ import useTime from "../../../hooks/useTime";
5
+ import { chatActions } from "../../../store/chatSlice";
6
+ import { modalActions } from "../../../store/modalSlice";
7
+ import { userProfileActions } from "../../../store/userProfileSlice";
8
+ import Header from "../../globals/Header";
9
+ import IconWrapper from "../../globals/IconWrapper";
10
+ import Image from "../../globals/Image";
11
+ import ActionsModal from "./ActionsModal";
12
+
13
+ function ChatHeader({ chatProfile, className }) {
14
+ const dispatch = useDispatch();
15
+ const chatActive = useSelector((state) => state.chatReducer.active);
16
+ const lastSeenTime = useTime(chatProfile?.status?.lastSeen);
17
+
18
+ return (
19
+ <Header
20
+ className={`flex items-center px-[2rem] bg-primary border-x border-border shrink-0 z-10 ${className}`}
21
+ >
22
+ <div className="flex items-center flex-grow">
23
+ <IconWrapper
24
+ id="chatActiveToggler"
25
+ onClick={() => {
26
+ if (chatActive) {
27
+ dispatch(chatActions.setChatUnactive());
28
+ } else {
29
+ dispatch(chatActions.setChatActive());
30
+ }
31
+ }}
32
+ className="chatArrow lg:flex mr-[2rem] hidden"
33
+ >
34
+ <svg
35
+ xmlns="http://www.w3.org/2000/svg"
36
+ width="1em"
37
+ height="1em"
38
+ preserveAspectRatio="xMidYMid meet"
39
+ viewBox="0 0 24 24"
40
+ className={`${!chatActive && "rotate-180"}`}
41
+ >
42
+ <path
43
+ fill="currentColor"
44
+ d="M19 11H7.14l3.63-4.36a1 1 0 1 0-1.54-1.28l-5 6a1.19 1.19 0 0 0-.09.15c0 .05 0 .08-.07.13A1 1 0 0 0 4 12a1 1 0 0 0 .07.36c0 .05 0 .08.07.13a1.19 1.19 0 0 0 .09.15l5 6A1 1 0 0 0 10 19a1 1 0 0 0 .64-.23a1 1 0 0 0 .13-1.41L7.14 13H19a1 1 0 0 0 0-2Z"
45
+ className="stroke-transparent"
46
+ />
47
+ </svg>
48
+ </IconWrapper>
49
+ {/* */}
50
+ <div
51
+ onClick={(event) => {
52
+ if (window.innerWidth <= 1000) {
53
+ if (chatActive) {
54
+ event.stopPropagation();
55
+
56
+ dispatch(userProfileActions.showProfile());
57
+ }
58
+ } else {
59
+ dispatch(userProfileActions.showProfile());
60
+ }
61
+ }}
62
+ className="flex-grow flex items-center gap-[1.5rem] cursor-pointer"
63
+ >
64
+ <Image
65
+ src={chatProfile.avatar}
66
+ alt={chatProfile.name || chatProfile.username}
67
+ className="w-[4.2rem] h-[4.2rem] rounded-full"
68
+ />
69
+ <div className="flex flex-col">
70
+ <h2 className="font-semibold">
71
+ {chatProfile.name || chatProfile.username}
72
+ </h2>
73
+ {chatProfile.mode && (
74
+ <span className="text-cta-icon italic text-[1.4rem] font-normal -translate-y-[.4rem]">
75
+ {chatProfile.mode} {chatProfile.mode === "recording" && "audio"}
76
+ ...
77
+ </span>
78
+ )}
79
+ {!chatProfile.mode && (
80
+ <span className="text-secondary text-[1.4rem] font-normal -translate-y-[.4rem]">
81
+ {chatProfile.status?.online
82
+ ? "online"
83
+ : `last seen at ${lastSeenTime}`}
84
+ </span>
85
+ )}
86
+ </div>
87
+ </div>
88
+ </div>
89
+ <IconWrapper
90
+ onClick={() => {
91
+ dispatch(
92
+ modalActions.openModal({
93
+ type: "actionsModal",
94
+ positions: { top: 60, right: 30 },
95
+ })
96
+ );
97
+ }}
98
+ >
99
+ <svg
100
+ xmlns="http://www.w3.org/2000/svg"
101
+ width="1em"
102
+ height="1em"
103
+ preserveAspectRatio="xMidYMid meet"
104
+ viewBox="0 0 24 24"
105
+ className="shrink-0"
106
+ >
107
+ <path
108
+ fill="currentColor"
109
+ d="M10 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0zm0-6a2 2 0 1 0 4 0a2 2 0 0 0-4 0zm0 12a2 2 0 1 0 4 0a2 2 0 0 0-4 0z"
110
+ className="!stroke-transparent"
111
+ />
112
+ </svg>
113
+ </IconWrapper>
114
+ <ActionsModal chatProfile={chatProfile} />
115
+ </Header>
116
+ );
117
+ }
118
+
119
+ export default ChatHeader;
client/src/components/pages/Chat/DayMessages.jsx ADDED
@@ -0,0 +1,57 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import { useEffect } from "react";
3
+ import { useState } from "react";
4
+ import { useSelector } from "react-redux";
5
+ import ReceivedMessage from "./ReceivedMessage";
6
+ import SentMessage from "./SentMessage";
7
+
8
+ function DayMessages({ messagesData }) {
9
+ const userId = useSelector((state) => state.userReducer.user._id);
10
+ const [day, setDay] = useState();
11
+
12
+ useEffect(() => {
13
+ const currentDay = new Date(Date.now()).toLocaleString("en-US", {
14
+ month: "long",
15
+ day: "2-digit",
16
+ year: "numeric",
17
+ });
18
+
19
+ const messageDay = new Date(messagesData.day).toLocaleString("en-US", {
20
+ month: "long",
21
+ day: "2-digit",
22
+ year: "numeric",
23
+ });
24
+
25
+ let outputDayString = "";
26
+
27
+ // Show Today if current day and message day is the same
28
+ if (messageDay === currentDay) {
29
+ outputDayString = "Today";
30
+ } else {
31
+ outputDayString = messageDay.split(",")[0];
32
+ }
33
+
34
+ setDay(outputDayString);
35
+ }, []);
36
+
37
+ return (
38
+ <div className="">
39
+ <p className="mx-auto w-fit py-[.2rem] px-[1rem] bg-message-highlight rounded-full my-[.5rem] font-semibold text-white">
40
+ {day}
41
+ </p>
42
+ <div className="flex flex-col items-start gap-[.5rem]">
43
+ {messagesData.messages.map((message) => (
44
+ <React.Fragment key={message._id}>
45
+ {message.sender !== userId ? (
46
+ <ReceivedMessage message={message} />
47
+ ) : (
48
+ <SentMessage message={message} />
49
+ )}
50
+ </React.Fragment>
51
+ ))}
52
+ </div>
53
+ </div>
54
+ );
55
+ }
56
+
57
+ export default DayMessages;
client/src/components/pages/Chat/EmojiModal.jsx ADDED
@@ -0,0 +1,39 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import EmojiPicker from "emoji-picker-react";
2
+ import React from "react";
3
+ import { AnimatePresence, motion } from "framer-motion";
4
+
5
+ function EmojiModal({ emojiVisible, setEmojiVisible, addEmojiToMessage }) {
6
+ const handleMouseMovement = (event) => {
7
+ event.currentTarget.addEventListener("mouseleave", () => {
8
+ setEmojiVisible(false);
9
+ });
10
+ };
11
+
12
+ return (
13
+ <AnimatePresence>
14
+ {emojiVisible && (
15
+ <motion.div
16
+ initial={{ scale: 0 }}
17
+ animate={{ scale: 1 }}
18
+ exit={{ scale: 0 }}
19
+ className={`absolute bottom-[9rem] left-0 w-[40rem] origin-bottom-left sm:static sm:origin-bottom mt-[.5rem] sm:!scale-100 sm:!duration-[1000ms] sm:w-full ${
20
+ !emojiVisible ? "sm:scale-y-0" : "sm:scale-y-100"
21
+ }`}
22
+ id="emojiPicker"
23
+ onMouseEnter={handleMouseMovement}
24
+ >
25
+ <EmojiPicker
26
+ width="100%"
27
+ lazyLoadEmojis={true}
28
+ // onEmojiClick={(emoji, getImageUrl) =>
29
+ // setTimeout(() => addEmojiToMessage(emoji, getImageUrl), 100)
30
+ // }
31
+ onEmojiClick={addEmojiToMessage}
32
+ />
33
+ </motion.div>
34
+ )}
35
+ </AnimatePresence>
36
+ );
37
+ }
38
+
39
+ export default EmojiModal;
client/src/components/pages/Chat/Message.jsx ADDED
@@ -0,0 +1,74 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import React from "react";
2
+ import CallMessage from "./CallMessage";
3
+ import MessageReadStatus from "./MessageReadStatus";
4
+ import VoiceMessage from "./VoiceMessage";
5
+ import Image from "../../globals/Image";
6
+
7
+ function Message({ messageData, className, messageReceived }) {
8
+ // Image messages
9
+ if (messageData.messageType === "image") {
10
+ return (
11
+ <div className="w-[30rem] rounded-3xl overflow-hidden h-[34rem] relative">
12
+ <Image
13
+ className="w-full h-full object-cover"
14
+ src={messageData.imageUrl}
15
+ alt=""
16
+ />
17
+ <MessageReadStatus
18
+ readStatus={messageData.readStatus}
19
+ deliveredStatus={messageData.deliveredStatus}
20
+ messageReceived={messageReceived}
21
+ time={messageData.timeSent}
22
+ className="absolute bottom-[1rem] right-[1rem] bg-secondary-light-text rounded-full !text-white"
23
+ />
24
+ </div>
25
+ );
26
+ }
27
+
28
+ if (messageData.messageType === "voice")
29
+ return (
30
+ <VoiceMessage
31
+ deliveredStatus={messageData.deliveredStatus}
32
+ messageReceived={messageReceived}
33
+ voiceDuration={messageData.voiceNoteDuration}
34
+ voiceNoteUrl={messageData.voiceNoteUrl}
35
+ readStatus={messageData.readStatus}
36
+ time={messageData.timeSent}
37
+ />
38
+ );
39
+
40
+ if (messageData.messageType === "call")
41
+ // Calls
42
+ return (
43
+ <CallMessage
44
+ callDetails={messageData.callDetails}
45
+ messageReceived={messageReceived}
46
+ deliveredStatus={messageData.deliveredStatus}
47
+ readStatus={messageData.readStatus}
48
+ time={messageData.timeSent}
49
+ />
50
+ );
51
+
52
+ // if it's a text message
53
+ return (
54
+ <div
55
+ className={`${className} p-[1.5rem] rounded-3xl sm:text-[1.4rem] overflow-hidden gap-[1rem] relative`}
56
+ >
57
+ <div
58
+ dangerouslySetInnerHTML={{ __html: messageData.message }}
59
+ className="font-semibold max-w-[25rem] mr-[3.5rem] break-words"
60
+ ></div>
61
+ <MessageReadStatus
62
+ readStatus={messageData.readStatus}
63
+ deliveredStatus={messageData.deliveredStatus}
64
+ messageReceived={messageReceived}
65
+ time={messageData.timeSent}
66
+ className={`absolute right-[.8rem] bottom-[.5rem] ${
67
+ messageData.deliveredStatus && "!text-secondary"
68
+ }`}
69
+ />
70
+ </div>
71
+ );
72
+ }
73
+
74
+ export default Message;