Spaces:
Paused
Paused
@ARAbdullaSL
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- .env +6 -0
- .gitattributes +1 -0
- Dockerfile +28 -0
- app.js +37 -0
- client/.env +7 -0
- client/package-lock.json +0 -0
- client/package.json +57 -0
- client/postcss.config.js +6 -0
- client/public/favicon.svg +1 -0
- client/public/index.html +49 -0
- client/public/logo192.png +0 -0
- client/public/logo512.png +0 -0
- client/public/manifest.json +25 -0
- client/public/robots.txt +3 -0
- client/src/App.jsx +49 -0
- client/src/App.test.js +8 -0
- client/src/AppRoutes.js +0 -0
- client/src/assets/Receiver Call Request Tone.mp3 +0 -0
- client/src/assets/Sender Call Request Tone.mp3 +3 -0
- client/src/assets/chat-bg-dark.jpg +0 -0
- client/src/assets/chat-bg-light.jpg +0 -0
- client/src/components/globals/CTAIconWrapper.jsx +14 -0
- client/src/components/globals/DeleteChat.jsx +50 -0
- client/src/components/globals/DeleteContact.jsx +59 -0
- client/src/components/globals/FormField.jsx +47 -0
- client/src/components/globals/Header.jsx +7 -0
- client/src/components/globals/IconWrapper.jsx +16 -0
- client/src/components/globals/Image.jsx +18 -0
- client/src/components/globals/MessageCheck.jsx +54 -0
- client/src/components/globals/Modal.jsx +45 -0
- client/src/components/globals/ModalChild.jsx +14 -0
- client/src/components/globals/NewContactForm.jsx +130 -0
- client/src/components/globals/Notification.jsx +59 -0
- client/src/components/globals/Overlay.jsx +27 -0
- client/src/components/globals/Sidebar.jsx +25 -0
- client/src/components/globals/Spinner.jsx +22 -0
- client/src/components/globals/VideoCallModal.jsx +142 -0
- client/src/components/globals/VoiceCallModal.jsx +137 -0
- client/src/components/pages/Authentication/Login.jsx +87 -0
- client/src/components/pages/Authentication/Register.jsx +146 -0
- client/src/components/pages/Chat/ActionsModal.jsx +106 -0
- client/src/components/pages/Chat/AttachFileModal.jsx +52 -0
- client/src/components/pages/Chat/AttachFileOrRecordDuration.jsx +53 -0
- client/src/components/pages/Chat/BubbleTail.jsx +22 -0
- client/src/components/pages/Chat/CTAButtons.jsx +194 -0
- client/src/components/pages/Chat/CallMessage.jsx +120 -0
- client/src/components/pages/Chat/ChatHeader.jsx +119 -0
- client/src/components/pages/Chat/DayMessages.jsx +57 -0
- client/src/components/pages/Chat/EmojiModal.jsx +39 -0
- 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 |
+
|
| 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;
|