├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── _redirects ├── favicons │ ├── android-chrome-192x192.png │ ├── android-chrome-512x512.png │ ├── apple-touch-icon.png │ ├── browserconfig.xml │ ├── favicon-16x16.png │ ├── favicon-32x32.png │ ├── favicon.ico │ ├── mstile-150x150.png │ ├── safari-pinned-tab.svg │ └── site.webmanifest ├── index.html └── robots.txt └── src ├── App.css ├── App.js ├── Audio ├── music.mp3 └── outgoing.mp3 ├── Components ├── CallUI │ ├── CallUI.css │ └── CallUI.js ├── Chats │ ├── Chats.css │ └── Chats.js ├── Home │ ├── Home.css │ └── Home.js ├── PageNotFound404 │ ├── PageNotFound404.css │ └── PageNotFound404.js ├── Room │ ├── Room.css │ └── Room.js └── Video │ ├── Video.css │ └── Video.js ├── Images └── logo.png ├── Utils └── truncate.js ├── index.css ├── index.js ├── normalize.css ├── reportWebVitals.js ├── service-worker.js ├── serviceWorkerRegistration.js └── setupTests.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | .env 22 | debug.log 23 | .eslintcache 24 | 25 | npm-debug.log* 26 | yarn-debug.log* 27 | yarn-error.log* 28 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Piyush Sati 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | 7 | 8 | 9 | 13 | 14 |
15 |
16 | 17 | 18 | # Call Buddy - Frontend 19 | 20 | Video Chat webapp built using React.js, Node.js, Socket.io and Peer.js 21 | 22 | 23 | 24 | - [Backend Code](https://github.com/Pinqua/Call-Buddy-Backend) 25 | 26 | 27 | 28 | 29 | ## Demo 30 | 31 | https://call-buddy.vercel.app 32 | 33 | 34 | ## Screenshots 35 | 36 | ![App Screenshot](https://i.ibb.co/qR2wSmn/call-buddy.gif) 37 | 38 | 39 | ## Features 40 | 41 | - Progressive Web App (PWA) 42 | - No sign up and sign in needed 43 | - Fully Responsive (android + desktop) 44 | - Instant messaging 45 | - All basic functionalities included like mute and unmute mic, video on and off, end call 46 | 47 | 48 | 49 | 50 | ## Run Locally 51 | 52 | Clone the project 53 | 54 | ```bash 55 | git clone https://github.com/Pinqua/Call-Buddy-Frontend.git 56 | ``` 57 | 58 | Go to the project directory 59 | 60 | ```bash 61 | cd Call-Buddy-Frontend 62 | ``` 63 | 64 | Install dependencies 65 | 66 | ```bash 67 | npm install 68 | ``` 69 | 70 | Create a **.env** file inside project directory with fields given below. 71 | 72 | ``` 73 | REACT_APP_SERVER_URL= add server url (wherever your backend code is hosted add its url over here) 74 | REACT_APP_PEERJS_HOST= add peerjs hosted server url 75 | 76 | GENERATE_SOURCEMAP=false 77 | ``` 78 | 79 | Start the server 80 | 81 | ```bash 82 | npm run start 83 | ``` 84 | 85 | 86 | ## Contributing 87 | 88 | Contributions are always welcome! 89 | 90 | ## License 91 | 92 | [MIT](https://choosealicense.com/licenses/mit/) 93 | 94 | 95 |
96 |
97 | 98 |

If you liked the repository, show your ❤️ by starring and forking it.

99 | 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "client", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@material-ui/core": "^4.11.2", 7 | "@material-ui/icons": "^4.11.2", 8 | "@testing-library/jest-dom": "^5.11.9", 9 | "@testing-library/react": "^11.2.3", 10 | "@testing-library/user-event": "^12.6.1", 11 | "moment": "^2.29.1", 12 | "peerjs": "^1.3.1", 13 | "react": "^17.0.1", 14 | "react-dom": "^17.0.1", 15 | "react-loader-spinner": "^3.1.14", 16 | "react-router-dom": "^5.2.0", 17 | "react-scripts": "4.0.1", 18 | "react-share": "^4.3.1", 19 | "socket.io-client": "^3.1.0", 20 | "web-vitals": "^0.2.4", 21 | "workbox-background-sync": "^5.1.4", 22 | "workbox-broadcast-update": "^5.1.4", 23 | "workbox-cacheable-response": "^5.1.4", 24 | "workbox-core": "^5.1.4", 25 | "workbox-expiration": "^5.1.4", 26 | "workbox-google-analytics": "^5.1.4", 27 | "workbox-navigation-preload": "^5.1.4", 28 | "workbox-precaching": "^5.1.4", 29 | "workbox-range-requests": "^5.1.4", 30 | "workbox-routing": "^5.1.4", 31 | "workbox-strategies": "^5.1.4", 32 | "workbox-streams": "^5.1.4" 33 | }, 34 | "scripts": { 35 | "start": "react-scripts start", 36 | "build": "react-scripts build", 37 | "test": "react-scripts test", 38 | "eject": "react-scripts eject" 39 | }, 40 | "eslintConfig": { 41 | "extends": [ 42 | "react-app", 43 | "react-app/jest" 44 | ] 45 | }, 46 | "browserslist": { 47 | "production": [ 48 | ">0.2%", 49 | "not dead", 50 | "not op_mini all" 51 | ], 52 | "development": [ 53 | "last 1 chrome version", 54 | "last 1 firefox version", 55 | "last 1 safari version" 56 | ] 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /public/_redirects: -------------------------------------------------------------------------------- 1 | /* /index.html 200 -------------------------------------------------------------------------------- /public/favicons/android-chrome-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/public/favicons/android-chrome-192x192.png -------------------------------------------------------------------------------- /public/favicons/android-chrome-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/public/favicons/android-chrome-512x512.png -------------------------------------------------------------------------------- /public/favicons/apple-touch-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/public/favicons/apple-touch-icon.png -------------------------------------------------------------------------------- /public/favicons/browserconfig.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | #ffffff 7 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /public/favicons/favicon-16x16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/public/favicons/favicon-16x16.png -------------------------------------------------------------------------------- /public/favicons/favicon-32x32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/public/favicons/favicon-32x32.png -------------------------------------------------------------------------------- /public/favicons/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/public/favicons/favicon.ico -------------------------------------------------------------------------------- /public/favicons/mstile-150x150.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/public/favicons/mstile-150x150.png -------------------------------------------------------------------------------- /public/favicons/safari-pinned-tab.svg: -------------------------------------------------------------------------------- 1 | 2 | 4 | 7 | 8 | Created by potrace 1.11, written by Peter Selinger 2001-2013 9 | 10 | 12 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /public/favicons/site.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Call Buddy", 3 | "short_name": "Call Buddy", 4 | "description": "Video Chat Progressive Web App build with ❤️ by Piyush Sati", 5 | "icons": [ 6 | { 7 | "src": "/favicons/android-chrome-192x192.png", 8 | "sizes": "192x192", 9 | "type": "image/png" 10 | }, 11 | { 12 | "src": "/favicons/android-chrome-512x512.png", 13 | "sizes": "512x512", 14 | "type": "image/png" 15 | } 16 | ], 17 | "start_url": "/", 18 | "theme_color": "#ffffff", 19 | "background_color": "#ffffff", 20 | "display": "standalone" 21 | } -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | Call Buddy 30 | 31 | 32 | 33 | 34 | 35 | 39 | 40 | 41 | 42 | 43 |
44 | 45 | 46 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .app { 2 | background-color: #111; 3 | width: 100%; 4 | height: 100%; 5 | color: white; 6 | } 7 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useRef, useCallback } from "react"; 2 | import "./App.css"; 3 | import Home from "./Components/Home/Home"; 4 | import { BrowserRouter as Router, Switch, Route } from "react-router-dom"; 5 | import Room from "./Components/Room/Room"; 6 | import PageNotFound404 from "./Components/PageNotFound404/PageNotFound404"; 7 | import io from "socket.io-client"; 8 | import Peer from "peerjs"; 9 | 10 | function App() { 11 | const [outgoingCall, setOutgoingCall] = useState(false); 12 | const [incomingCall, setIncomingCall] = useState(false); 13 | const socket = useRef(); 14 | const [, setCam] = useState("user"); 15 | const [notificationMsg, setNotificationMsg] = useState(""); 16 | const [userInfo, setUserInfo] = useState({ 17 | id: "", 18 | displayName: "", 19 | stream: null, 20 | videoCamOff: false, 21 | micOff: false, 22 | }); 23 | const [myInfo, setMyInfo] = useState({ 24 | id: "", 25 | displayName: "", 26 | stream: null, 27 | videoCamOff: false, 28 | micOff: false, 29 | }); 30 | const myPeer = useRef( 31 | new Peer( 32 | /*undefined, { host: "localhost", port: 5000, path: "/myapp" }*/ 33 | undefined, 34 | { 35 | host: `${process.env.REACT_APP_PEERJS_HOST}`, 36 | port: 443, 37 | path: "/", 38 | secure: true, 39 | } 40 | ) 41 | ); 42 | 43 | const answerCall = useCallback(() => { 44 | socket.current.emit("call-attended", myInfo, userInfo.id); 45 | setIncomingCall(false); 46 | }, [myInfo, userInfo.id]); 47 | 48 | const callUser = useCallback( 49 | (userID) => { 50 | if (myInfo.id === userID) { 51 | setNotificationMsg("Enter someone else ID"); 52 | return; 53 | } 54 | socket.current.emit("call-user", { ...myInfo, stream: null }, userID); 55 | setOutgoingCall(true); 56 | setUserInfo((userInfo) => ({ ...userInfo, id: userID })); 57 | }, 58 | [myInfo] 59 | ); 60 | 61 | const displayNameHandler = useCallback((displayName) => { 62 | setMyInfo((myInfo) => ({ ...myInfo, displayName: displayName })); 63 | }, []); 64 | 65 | const endCall = useCallback(() => { 66 | socket.current.emit("end-call", userInfo.id); 67 | setUserInfo({ 68 | id: "", 69 | displayName: "", 70 | stream: null, 71 | videoCamOff: false, 72 | micOff: false, 73 | }); 74 | }, [userInfo.id]); 75 | 76 | const rejectCall = useCallback(() => { 77 | socket.current.emit("call-rejected", userInfo.id); 78 | setIncomingCall(false); 79 | setOutgoingCall(false); 80 | setUserInfo({ 81 | id: "", 82 | displayName: "", 83 | stream: null, 84 | videoCamOff: false, 85 | micOff: false, 86 | }); 87 | }, [userInfo.id]); 88 | 89 | const videoCamOffHandler = useCallback(() => { 90 | if (userInfo.stream) { 91 | socket.current.emit( 92 | "user-info", 93 | { ...myInfo, videoCamOff: !myInfo.videoCamOff }, 94 | userInfo.id 95 | ); 96 | } 97 | setMyInfo((myInfo) => ({ ...myInfo, videoCamOff: !myInfo.videoCamOff })); 98 | }, [myInfo, userInfo.id, userInfo.stream]); 99 | 100 | const micOffHandler = useCallback(() => { 101 | if (userInfo.stream) { 102 | socket.current.emit( 103 | "user-info", 104 | { ...myInfo, micOff: !myInfo.micOff }, 105 | userInfo.id 106 | ); 107 | } 108 | setMyInfo((myInfo) => ({ ...myInfo, micOff: !myInfo.micOff })); 109 | }, [userInfo.stream, userInfo.id, myInfo]); 110 | 111 | const switchCamera = useCallback(() => { 112 | const supported = navigator.mediaDevices.getSupportedConstraints(); 113 | if (!supported["facingMode"]) { 114 | alert("Not supported by browser"); 115 | return; 116 | } 117 | setCam((cam) => { 118 | if (cam === "user") { 119 | socket.current.emit("switch-cam", "environment", userInfo.id); 120 | return "environment"; 121 | } 122 | socket.current.emit("switch-cam", "user", userInfo.id); 123 | return "user"; 124 | }); 125 | }, [userInfo.id]); 126 | 127 | //automatically hide notification message after some time. 128 | useEffect(() => { 129 | let timeOut = null; 130 | if (notificationMsg) { 131 | timeOut = setTimeout(() => { 132 | setNotificationMsg(""); 133 | }, 4000); 134 | } 135 | return () => { 136 | clearTimeout(timeOut); 137 | }; 138 | }, [notificationMsg]); 139 | 140 | //automatically cut the call after some time. 141 | useEffect(() => { 142 | let timeout = null; 143 | if (outgoingCall) { 144 | timeout = setTimeout(() => { 145 | rejectCall(); 146 | setNotificationMsg( 147 | "User doesn't exist or user is not picking up your call" 148 | ); 149 | }, 60000); 150 | } 151 | return () => { 152 | if (timeout) { 153 | clearTimeout(timeout); 154 | } 155 | }; 156 | }, [outgoingCall, rejectCall]); 157 | 158 | useEffect(() => { 159 | socket.current = io(`${process.env.REACT_APP_SERVER_URL}/media`); 160 | //socket.current = io("http://localhost:9000/media"); 161 | 162 | let Stream; 163 | 164 | function CALL(userID, stream) { 165 | const call = myPeer.current.call(userID, stream); 166 | call.on("stream", (userStream) => { 167 | setUserInfo((userInfo) => ({ ...userInfo, stream: userStream })); 168 | }); 169 | call.on("close", () => { 170 | setUserInfo({ 171 | id: "", 172 | displayName: "", 173 | stream: null, 174 | videoCamOff: false, 175 | micOff: false, 176 | }); 177 | }); 178 | } 179 | 180 | navigator.mediaDevices 181 | .getUserMedia({ 182 | video: { 183 | facingMode: "user", 184 | width: { min: 160, ideal: 640, max: 1280 }, 185 | height: { min: 120, ideal: 360, max: 720 }, 186 | aspectRatio: { ideal: 1.7777777778 }, 187 | }, 188 | audio: { 189 | sampleRate: { ideal: 48000 }, 190 | sampleSize: { ideal: 16 }, 191 | channelCount: { ideal: 2, min: 1 }, 192 | echoCancellation: true, 193 | noiseSuppression: true, 194 | autoGainControl: true, 195 | }, 196 | }) 197 | .then((stream) => { 198 | setMyInfo((myInfo) => ({ ...myInfo, stream: stream })); 199 | Stream = stream; 200 | myPeer.current.on("call", (call) => { 201 | call.answer(stream); 202 | call.on("stream", (userStream) => { 203 | setUserInfo((userInfo) => ({ ...userInfo, stream: userStream })); 204 | }); 205 | }); 206 | socket.current.on("call--attended", (userInfo) => { 207 | setUserInfo((usrInfo) => ({ ...userInfo, stream: usrInfo.stream })); 208 | CALL(userInfo.id, stream); 209 | setOutgoingCall(false); 210 | }); 211 | }) 212 | .catch((err) => { 213 | console.log(err); 214 | alert(err); 215 | }); 216 | 217 | // on open will be launch when you successfully connect to PeerServer 218 | myPeer.current.on("open", (id) => { 219 | setMyInfo((myInfo) => ({ ...myInfo, id: id })); 220 | socket.current.emit("join-media", id); 221 | myPeer.current.on("error", (err) => { 222 | console.log(err); 223 | alert(err); 224 | setUserInfo((userInfo) => { 225 | socket.current.emit("end-call", userInfo.id); 226 | return { 227 | id: "", 228 | displayName: "", 229 | stream: null, 230 | videoCamOff: false, 231 | micOff: false, 232 | }; 233 | }); 234 | }); 235 | }); 236 | 237 | myPeer.current.on("disconnected", () => { 238 | setUserInfo((userInfo) => { 239 | socket.current.emit("end-call", userInfo.id); 240 | return { 241 | id: "", 242 | displayName: "", 243 | stream: null, 244 | videoCamOff: false, 245 | micOff: false, 246 | }; 247 | }); 248 | }); 249 | 250 | myPeer.current.on("close", () => { 251 | setUserInfo((userInfo) => { 252 | socket.current.emit("end-call", userInfo.id); 253 | return { 254 | id: "", 255 | displayName: "", 256 | stream: null, 257 | videoCamOff: false, 258 | micOff: false, 259 | }; 260 | }); 261 | }); 262 | 263 | socket.current.on("connect", () => { 264 | socket.current.on("incoming-call", (userInfo) => { 265 | setUserInfo((usrInfo) => { 266 | if (usrInfo.stream || usrInfo.id) { 267 | socket.current.emit("busy", userInfo.id); 268 | if (usrInfo.stream) { 269 | setIncomingCall(false); 270 | } 271 | return usrInfo; 272 | } 273 | setIncomingCall(true); 274 | return userInfo; 275 | }); 276 | }); 277 | 278 | socket.current.on("user-busy", () => { 279 | setOutgoingCall(false); 280 | setNotificationMsg("User is busy"); 281 | setUserInfo({ 282 | id: "", 283 | displayName: "", 284 | stream: null, 285 | videoCamOff: false, 286 | micOff: false, 287 | }); 288 | }); 289 | socket.current.on("user--info", (userInfo) => { 290 | setUserInfo((usrInfo) => ({ ...userInfo, stream: usrInfo.stream })); 291 | }); 292 | socket.current.on("call--rejected", () => { 293 | setOutgoingCall(false); 294 | setIncomingCall(false); 295 | setUserInfo({ 296 | id: "", 297 | displayName: "", 298 | stream: null, 299 | videoCamOff: false, 300 | micOff: false, 301 | }); 302 | }); 303 | socket.current.on("call-ended", () => { 304 | setUserInfo({ 305 | id: "", 306 | displayName: "", 307 | stream: null, 308 | videoCamOff: false, 309 | micOff: false, 310 | }); 311 | }); 312 | 313 | socket.current.on("user-disconnected", (ID) => { 314 | setUserInfo((Info) => { 315 | if (ID === Info.id) { 316 | return { ...Info, stream: null }; 317 | } 318 | return { ...Info }; 319 | }); 320 | }); 321 | 322 | socket.current.on("switch-camera", (camera, userID) => { 323 | //stop the tracks 324 | const tracks = Stream.getTracks(); 325 | tracks.forEach((tracks) => tracks.stop()); 326 | navigator.mediaDevices 327 | .getUserMedia({ 328 | video: { 329 | facingMode: camera, 330 | width: { min: 160, ideal: 640, max: 1280 }, 331 | height: { min: 120, ideal: 360, max: 720 }, 332 | aspectRatio: { ideal: 1.7777777778 }, 333 | }, 334 | audio: { 335 | sampleRate: { ideal: 48000 }, 336 | sampleSize: { ideal: 16 }, 337 | channelCount: { ideal: 2, min: 1 }, 338 | echoCancellation: true, 339 | noiseSuppression: true, 340 | autoGainControl: true, 341 | }, 342 | }) 343 | .then((stream) => { 344 | Stream = stream; 345 | setMyInfo((myInfo) => ({ ...myInfo, stream: stream })); 346 | CALL(userID, stream); 347 | }) 348 | .catch((err) => { 349 | console.log(err); 350 | alert(err); 351 | }); 352 | }); 353 | }); 354 | 355 | const SOCKET = socket.current; 356 | return () => { 357 | SOCKET.removeAllListeners(); 358 | }; 359 | }, []); 360 | 361 | //to stop echo and mute my audio 362 | useEffect(() => { 363 | if (myInfo.stream && myInfo.stream.getAudioTracks().length > 0) { 364 | myInfo.stream.getAudioTracks().forEach((track) => { 365 | track.enabled = false; 366 | }); 367 | } 368 | }, [myInfo.stream]); 369 | 370 | return ( 371 |
372 | 373 | 374 | 375 | {!userInfo.stream ? ( 376 | 389 | ) : ( 390 | 398 | )} 399 | 400 | 401 | 402 | 403 | 404 | 405 |
406 | ); 407 | } 408 | 409 | export default App; 410 | -------------------------------------------------------------------------------- /src/Audio/music.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/src/Audio/music.mp3 -------------------------------------------------------------------------------- /src/Audio/outgoing.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/src/Audio/outgoing.mp3 -------------------------------------------------------------------------------- /src/Components/CallUI/CallUI.css: -------------------------------------------------------------------------------- 1 | @-webkit-keyframes callAnimation { 2 | from { 3 | transform: scale(1.05); 4 | } 5 | to { 6 | transform: scale(1); 7 | } 8 | } 9 | 10 | @keyframes callAnimation { 11 | from { 12 | transform: scale(1.05); 13 | } 14 | to { 15 | transform: scale(1); 16 | } 17 | } 18 | 19 | @-webkit-keyframes call-animation { 20 | from { 21 | right: -100%; 22 | } 23 | to { 24 | right: 4%; 25 | } 26 | } 27 | 28 | @keyframes call-animation { 29 | from { 30 | right: -100%; 31 | } 32 | to { 33 | right: 4%; 34 | } 35 | } 36 | 37 | .callUI { 38 | position: fixed; 39 | top: 40px; 40 | right: 4%; 41 | padding: 20px; 42 | z-index: 999; 43 | border-radius: 8px; 44 | background-color: #111111e8; 45 | color: white; 46 | font-size: 0.9rem; 47 | max-width: 380px; 48 | width: 380px; 49 | } 50 | .call__incomingCall { 51 | -webkit-animation: callAnimation 0.6s infinite, call-animation 0.5s; 52 | animation: callAnimation 0.6s infinite, call-animation 0.5s; 53 | } 54 | .call__outgoingCall { 55 | -webkit-animation: call-animation 0.5s; 56 | animation: call-animation 0.5s; 57 | } 58 | 59 | .call__options { 60 | display: flex; 61 | align-items: center; 62 | justify-content: center; 63 | } 64 | 65 | .call__option { 66 | background-color: #5277f0; 67 | width: 50px; 68 | height: 50px; 69 | margin: 0 10px; 70 | display: flex; 71 | justify-content: center; 72 | align-items: center; 73 | border-radius: 50%; 74 | cursor: pointer; 75 | } 76 | .callUI .call__end { 77 | background-color: #ff0000; 78 | } 79 | .callUI .call__answer { 80 | background-color: #10af10; 81 | } 82 | .call__user { 83 | display: flex; 84 | flex-direction: column; 85 | justify-content: center; 86 | margin-bottom: 20px; 87 | } 88 | .call__userID { 89 | margin: 5px 0; 90 | line-height: 1.2rem; 91 | word-break: break-all; 92 | word-wrap: break-word; 93 | } 94 | .call__info { 95 | padding: 10px 0; 96 | text-align: center; 97 | } 98 | 99 | .callUI .call__option > .MuiSvgIcon-root { 100 | color: white; 101 | } 102 | 103 | @media screen and (max-width: 400px) { 104 | .callUI { 105 | max-width: 90%; 106 | width: 90%; 107 | min-width: 290px; 108 | } 109 | .call__option { 110 | width: 48px; 111 | height: 48px; 112 | margin: 0 8px; 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/Components/CallUI/CallUI.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./CallUI.css"; 3 | import { 4 | Call, 5 | CallEnd, 6 | Mic, 7 | MicOff, 8 | Videocam, 9 | VideocamOff, 10 | } from "@material-ui/icons"; 11 | import { truncate } from "../../Utils/truncate"; 12 | 13 | function CallUI({ 14 | displayName, 15 | id, 16 | incoming, 17 | music, 18 | answerCall, 19 | rejectCall, 20 | myInfo, 21 | videoCamOffHandler, 22 | micOffHandler, 23 | }) { 24 | return ( 25 |
30 |
31 | {truncate(displayName, 30)} 32 |
ID - {id}
33 | {incoming ? "Incoming" : "Calling"} 34 |
35 |
36 | {incoming && ( 37 |
38 | 39 |
40 | )} 41 | {!incoming && ( 42 |
43 | 44 |
45 | )} 46 |
47 | {myInfo.videoCamOff ? : } 48 |
49 |
50 | {myInfo.micOff ? : } 51 |
52 | {incoming && ( 53 |
54 | 55 |
56 | )} 57 |
58 | {/* music for incoming and outgoing calls */} 59 | 65 |
66 | ); 67 | } 68 | 69 | export default CallUI; 70 | -------------------------------------------------------------------------------- /src/Components/Chats/Chats.css: -------------------------------------------------------------------------------- 1 | .chats__container { 2 | background-color: #1b1b1b; 3 | position: fixed; 4 | bottom: 10px; 5 | right: 10px; 6 | max-width: 400px; 7 | width: 100%; 8 | z-index: 9999; 9 | display: flex; 10 | flex-direction: column; 11 | transition: max-height 0.4s; 12 | height: 100%; 13 | max-height: 500px; 14 | border-radius: 10px; 15 | overflow: hidden; 16 | } 17 | .chats__top { 18 | background-color: black; 19 | font-size: 1.3rem; 20 | padding: 15px; 21 | position: relative; 22 | } 23 | .chats__close { 24 | position: absolute; 25 | top: 6px; 26 | right: 10px; 27 | cursor: pointer; 28 | } 29 | .chats { 30 | flex: 1; 31 | padding: 30px 20px; 32 | overflow: auto; 33 | -ms-overflow-style: none; /* IE and Edge */ 34 | scrollbar-width: none; 35 | } 36 | .chats::-webkit-scrollbar { 37 | display: none; 38 | } 39 | .chats__bottom { 40 | background-color: black; 41 | padding: 10px; 42 | } 43 | .chats__bottom > form { 44 | display: flex; 45 | } 46 | .chats__bottom input { 47 | flex: 1; 48 | padding: 10px 15px; 49 | border: none; 50 | font-size: 0.9rem; 51 | line-height: 1; 52 | } 53 | .chat__submit { 54 | padding: 10px 20px; 55 | border: none; 56 | outline: none; 57 | background-color: #5277f0; 58 | color: white; 59 | border-bottom-right-radius: 3px; 60 | border-top-right-radius: 3px; 61 | } 62 | .chats__message { 63 | margin-bottom: 3rem; 64 | border-radius: 5px; 65 | position: relative; 66 | word-wrap: break-word; 67 | font-size: 0.9rem; 68 | } 69 | .chats__username { 70 | font-size: 0.7rem; 71 | position: absolute; 72 | top: -20px; 73 | left: 0; 74 | } 75 | .chats__time { 76 | text-align: left; 77 | font-size: 0.7rem; 78 | position: absolute; 79 | bottom: -15px; 80 | right: 0; 81 | } 82 | 83 | @media screen and (max-width: 400px) { 84 | .chats__container { 85 | right: 0; 86 | left: 0; 87 | bottom: 0; 88 | max-width: 100%; 89 | min-width: 320px; 90 | max-height: 80%; 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /src/Components/Chats/Chats.js: -------------------------------------------------------------------------------- 1 | import { Close, Send } from "@material-ui/icons"; 2 | import React, { useEffect, useRef, useState } from "react"; 3 | import "./Chats.css"; 4 | import io from "socket.io-client"; 5 | import { v4 as uuidv4 } from "uuid"; 6 | import moment from "moment"; 7 | 8 | function Chats({ 9 | chatsHandler, 10 | addStyle, 11 | userID, 12 | myID, 13 | notifyHandler, 14 | displayName, 15 | }) { 16 | const [chats, setChats] = useState([]); 17 | const [msg, setMsg] = useState(""); 18 | const socket = useRef(); 19 | const chatsContainer = useRef(); 20 | 21 | const chatHandler = (e) => { 22 | e.preventDefault(); 23 | const new_msg = { 24 | id: uuidv4(), 25 | name: displayName, 26 | msg: msg, 27 | time: moment().format("LLL"), 28 | }; 29 | //send msg 30 | socket.current.emit("chat-message", new_msg, userID); 31 | setChats((chats) => [...chats, new_msg]); 32 | setMsg(""); 33 | //scroll to bottom of chats 34 | chatsContainer.current.scrollTo( 35 | 0, 36 | chatsContainer.current.scrollHeight - chatsContainer.current.clientHeight 37 | ); 38 | }; 39 | 40 | useEffect(() => { 41 | socket.current = io(`${process.env.REACT_APP_SERVER_URL}/chats`); 42 | //socket.current = io("https://localhost:9000/chats"); 43 | socket.current.on("connect", () => { 44 | socket.current.emit("join-chat", myID); 45 | //received new chat messages 46 | socket.current.on("chat-msg", (msg) => { 47 | setChats((chats) => [...chats, msg]); 48 | notifyHandler(); 49 | chatsContainer.current.scrollTo( 50 | 0, 51 | chatsContainer.current.scrollHeight - 52 | chatsContainer.current.clientHeight 53 | ); 54 | }); 55 | }); 56 | 57 | chatsContainer.current.scrollTo( 58 | 0, 59 | chatsContainer.current.scrollHeight - chatsContainer.current.clientHeight 60 | ); 61 | 62 | const Socket = socket.current; 63 | return () => { 64 | Socket.removeAllListeners(); 65 | }; 66 | }, [myID, notifyHandler]); 67 | 68 | return ( 69 |
70 |
71 | Chats 72 | 73 |
74 |
75 | {chats.map((msg) => ( 76 |
77 |
{msg.name}
78 | {msg.msg} 79 |
{msg.time}
80 |
81 | ))} 82 |
83 |
84 |
85 | setMsg(e.target.value)} 91 | /> 92 | 95 |
96 |
97 |
98 | ); 99 | } 100 | 101 | export default Chats; 102 | -------------------------------------------------------------------------------- /src/Components/Home/Home.css: -------------------------------------------------------------------------------- 1 | .home { 2 | width: 100%; 3 | height: 100%; 4 | position: relative; 5 | background-color: white; 6 | color: black; 7 | } 8 | .home__content { 9 | padding: 9rem 2% 4rem 2%; 10 | display: flex; 11 | flex-direction: column; 12 | align-items: center; 13 | } 14 | 15 | .home .logo { 16 | position: absolute; 17 | top: 3%; 18 | left: 2%; 19 | } 20 | .home .logo img { 21 | -o-object-fit: contain; 22 | object-fit: contain; 23 | width: 100px; 24 | } 25 | 26 | .home__content h1 { 27 | font-weight: 400; 28 | font-size: 2rem; 29 | margin: auto; 30 | text-align: center; 31 | } 32 | .home__content h1 span { 33 | color: #5277f0; 34 | } 35 | 36 | .home__box { 37 | max-width: 450px; 38 | padding: 25px 25px 3rem 25px; 39 | width: 100%; 40 | background-color: white; 41 | box-shadow: 0 4px 8px 0 #dddddd, 0 6px 20px 0 #ddddddea; 42 | border: 1px solid #ddddddea; 43 | height: 100%; 44 | border-radius: 12px; 45 | } 46 | 47 | .home__box h3, 48 | .model__content h3 { 49 | font-weight: 500; 50 | margin-bottom: 10px; 51 | } 52 | 53 | .home__box .myUserID { 54 | margin: 20px 0; 55 | padding: 15px; 56 | border: 2px dashed #5277f0; 57 | border-radius: 12px; 58 | position: relative; 59 | } 60 | .home__box .copyIcon { 61 | position: absolute; 62 | top: 8px; 63 | right: 8px; 64 | color: #5f5f5f; 65 | cursor: pointer; 66 | } 67 | .home__box .copyIcon:hover { 68 | color: black; 69 | } 70 | 71 | .home__box .myUserID span { 72 | line-height: 1.4rem; 73 | color: #5277f0; 74 | word-break: break-all; 75 | font-size: 1.1rem; 76 | } 77 | 78 | .home input { 79 | padding: 18px; 80 | border: none; 81 | width: 100%; 82 | border-radius: 8px; 83 | font-size: 1.1rem; 84 | line-height: 1; 85 | background-color: #eeeeee; 86 | margin-bottom: 20px; 87 | } 88 | 89 | .home button { 90 | padding: 15px 30px; 91 | border: none; 92 | outline: none; 93 | background-color: #5277f0; 94 | color: white; 95 | border-radius: 8px; 96 | width: 100%; 97 | cursor: pointer; 98 | display: flex; 99 | font-size: 1.1rem; 100 | width: 100%; 101 | justify-content: center; 102 | align-items: center; 103 | } 104 | .home button:hover { 105 | background-color: #345de2; 106 | } 107 | .home button .MuiSvgIcon-root { 108 | font-size: inherit; 109 | } 110 | .video__box .home__callOptions { 111 | display: flex; 112 | align-items: center; 113 | justify-content: center; 114 | } 115 | 116 | .video__box .home__callOption { 117 | background-color: #5277f0; 118 | width: 50px; 119 | height: 50px; 120 | margin: 0 15px; 121 | display: flex; 122 | justify-content: center; 123 | align-items: center; 124 | border-radius: 50%; 125 | cursor: pointer; 126 | } 127 | 128 | .video__box .home__callOption > .MuiSvgIcon-root { 129 | color: white; 130 | } 131 | .home__share { 132 | display: flex; 133 | margin-top: 10px; 134 | align-items: center; 135 | } 136 | 137 | .home .video__box { 138 | position: relative; 139 | overflow: hidden; 140 | max-height: 200px; 141 | border-radius: 12px; 142 | margin-bottom: 20px; 143 | } 144 | 145 | .video__box .home__callOptions { 146 | position: absolute; 147 | bottom: 15px; 148 | left: 0; 149 | right: 0; 150 | } 151 | 152 | .home__box .generating__id { 153 | display: flex; 154 | justify-content: center; 155 | align-items: center; 156 | flex-direction: column; 157 | } 158 | 159 | @-webkit-keyframes modelAnimation { 160 | from { 161 | transform: scale(0); 162 | opacity: 0; 163 | } 164 | to { 165 | transform: scale(1); 166 | opacity: 1; 167 | } 168 | } 169 | 170 | @keyframes modelAnimation { 171 | from { 172 | transform: scale(0); 173 | opacity: 0; 174 | } 175 | to { 176 | transform: scale(1); 177 | opacity: 1; 178 | } 179 | } 180 | 181 | .model { 182 | position: fixed; 183 | background-color: #000000e6; 184 | top: 0; 185 | bottom: 0; 186 | left: 0; 187 | right: 0; 188 | display: flex; 189 | justify-content: center; 190 | align-items: center; 191 | z-index: 99999; 192 | } 193 | 194 | .model .model__content { 195 | padding: 2rem 20px; 196 | border-radius: 6px; 197 | background-color: white; 198 | -webkit-animation: modelAnimation 0.6s; 199 | animation: modelAnimation 0.6s; 200 | max-width: 450px; 201 | width: 100%; 202 | margin: 15px; 203 | } 204 | 205 | .model__content h3 { 206 | margin-bottom: 20px; 207 | } 208 | .model__content h3 span { 209 | color: #5277f0; 210 | } 211 | 212 | .model__content p { 213 | margin-top: 20px; 214 | text-align: center; 215 | font-size: 0.9rem; 216 | } 217 | 218 | @-webkit-keyframes notification-animation { 219 | from { 220 | right: -100%; 221 | } 222 | to { 223 | right: 4%; 224 | } 225 | } 226 | @keyframes notification-animation { 227 | from { 228 | right: -100%; 229 | } 230 | to { 231 | right: 4%; 232 | } 233 | } 234 | 235 | .notification { 236 | position: fixed; 237 | top: 20px; 238 | right: 4%; 239 | z-index: 999; 240 | background-color: #111111e8; 241 | color: white; 242 | max-width: 350px; 243 | padding: 20px; 244 | border-radius: 6px; 245 | -webkit-animation: notification-animation 0.5s; 246 | animation: notification-animation 0.5s; 247 | } 248 | 249 | @media screen and (max-width: 800px) { 250 | .home .logo img { 251 | width: 90px; 252 | } 253 | } 254 | 255 | @media screen and (max-width: 400px) { 256 | .notification { 257 | max-width: 290px; 258 | } 259 | .home .logo img { 260 | width: 80px; 261 | } 262 | .home__box { 263 | padding: 5% 5% 2.5rem 5%; 264 | } 265 | .home__content { 266 | padding: 7rem 2% 4rem 2%; 267 | } 268 | .model .model__content { 269 | min-width: 280px; 270 | } 271 | .video__box .home__callOption { 272 | width: 48px; 273 | height: 48px; 274 | } 275 | } 276 | -------------------------------------------------------------------------------- /src/Components/Home/Home.js: -------------------------------------------------------------------------------- 1 | import React, { useEffect, useRef, useState } from "react"; 2 | import "./Home.css"; 3 | import logo from "../../Images/logo.png"; 4 | import { Link } from "react-router-dom"; 5 | import Video from "../Video/Video"; 6 | import { 7 | Call, 8 | FileCopy, 9 | Mic, 10 | MicOff, 11 | Videocam, 12 | VideocamOff, 13 | } from "@material-ui/icons"; 14 | import music from "../../Audio/music.mp3"; 15 | import OutgoingMusic from "../../Audio/outgoing.mp3"; 16 | import CallUI from "../CallUI/CallUI"; 17 | import Loader from "react-loader-spinner"; 18 | import { 19 | EmailShareButton, 20 | EmailIcon, 21 | FacebookShareButton, 22 | FacebookIcon, 23 | LinkedinShareButton, 24 | LinkedinIcon, 25 | TelegramShareButton, 26 | TelegramIcon, 27 | TwitterShareButton, 28 | TwitterIcon, 29 | FacebookMessengerShareButton, 30 | FacebookMessengerIcon, 31 | WhatsappShareButton, 32 | WhatsappIcon, 33 | } from "react-share"; 34 | 35 | function Home({ 36 | callUser, 37 | answerCall, 38 | rejectCall, 39 | incomingCall, 40 | outgoingCall, 41 | myInfo, 42 | userInfo, 43 | videoCamOffHandler, 44 | micOffHandler, 45 | displayNameHandler, 46 | notificationMsg, 47 | }) { 48 | const [inputUserID, setInputUserID] = useState(""); 49 | const [inputDisplayName, setInputDisplayName] = useState(""); 50 | const itemToCopy = useRef(); 51 | const [copied, setCopied] = useState(false); 52 | 53 | const formHandler = (e) => { 54 | e.preventDefault(); 55 | callUser(inputUserID); 56 | setInputUserID(""); 57 | }; 58 | 59 | //user display name 60 | const DisplayNameHandler = (e) => { 61 | e.preventDefault(); 62 | displayNameHandler(inputDisplayName); 63 | }; 64 | 65 | //copy text to clipboard 66 | const copyToClipboard = () => { 67 | const str = itemToCopy.current.innerText; 68 | const el = document.createElement("textarea"); 69 | el.value = str; 70 | el.setAttribute("readonly", ""); 71 | el.style.position = "absolute"; 72 | el.style.left = "-9999px"; 73 | document.body.appendChild(el); 74 | el.select(); 75 | document.execCommand("copy"); 76 | document.body.removeChild(el); 77 | setCopied(true); 78 | }; 79 | 80 | useEffect(() => { 81 | let timeout = null; 82 | if (copied) { 83 | timeout = setTimeout(() => { 84 | setCopied(false); 85 | }, 2000); 86 | } 87 | return () => { 88 | if (timeout) { 89 | clearTimeout(timeout); 90 | } 91 | }; 92 | }, [copied]); 93 | 94 | return ( 95 |
96 |
97 | 98 | 99 | 100 |
101 |
102 |
103 |
104 |
125 |
126 | {myInfo.id ? ( 127 | <> 128 |

My ID

129 | {myInfo.id} 130 | 131 |
132 | 133 | 137 | 138 | 139 | 143 | 144 | 145 | 149 | 150 | 151 | 155 | 156 | 157 | 161 | 162 | 163 | 167 | 168 | 169 | 173 | 174 |
175 | 176 | ) : ( 177 | <> 178 |

Generating ID

179 | 180 | 181 | )} 182 |
183 |
184 | setInputUserID(e.target.value)} 190 | /> 191 | 195 |
196 |
197 |
198 | {incomingCall && ( 199 | 210 | )} 211 | {outgoingCall && ( 212 | 221 | )} 222 | {!myInfo.displayName && ( 223 |
224 |
225 |

226 | Welcome to Call Buddy. 227 |

228 |
229 | { 235 | setInputDisplayName(e.target.value); 236 | }} 237 | /> 238 | 239 |
240 |

Build with ❤️ by Piyush Sati

241 |
242 |
243 | )} 244 | {(notificationMsg || copied) && ( 245 |
246 |

{notificationMsg ? notificationMsg : "Copied"}

247 |
248 | )} 249 |
250 | ); 251 | } 252 | 253 | export default Home; 254 | -------------------------------------------------------------------------------- /src/Components/PageNotFound404/PageNotFound404.css: -------------------------------------------------------------------------------- 1 | .notFound404 { 2 | position: relative; 3 | height: 100vh; 4 | } 5 | 6 | .notFound404 .notfound { 7 | position: absolute; 8 | left: 50%; 9 | top: 50%; 10 | -webkit-transform: translate(-50%, -50%); 11 | -ms-transform: translate(-50%, -50%); 12 | transform: translate(-50%, -50%); 13 | } 14 | 15 | .notfound { 16 | max-width: 520px; 17 | width: 100%; 18 | line-height: 1.4; 19 | text-align: center; 20 | } 21 | 22 | .notfound .notfound-404 { 23 | position: relative; 24 | height: 240px; 25 | } 26 | 27 | .notfound .notfound-404 h1 { 28 | font-family: "Montserrat", sans-serif; 29 | position: absolute; 30 | left: 50%; 31 | top: 50%; 32 | -webkit-transform: translate(-50%, -50%); 33 | -ms-transform: translate(-50%, -50%); 34 | transform: translate(-50%, -50%); 35 | font-size: 252px; 36 | font-weight: 900; 37 | margin: 0px; 38 | color: #262626; 39 | text-transform: uppercase; 40 | letter-spacing: -40px; 41 | margin-left: -20px; 42 | } 43 | 44 | .notfound .notfound-404 h1 > span { 45 | text-shadow: -8px 0px 0px #fff; 46 | } 47 | 48 | .notfound .notfound-404 h3 { 49 | font-family: "Cabin", sans-serif; 50 | position: relative; 51 | font-size: 16px; 52 | font-weight: 700; 53 | text-transform: uppercase; 54 | /*color: #262626;*/ 55 | color: white; 56 | margin: 0px; 57 | letter-spacing: 3px; 58 | padding-left: 6px; 59 | } 60 | 61 | .notfound h2 { 62 | font-family: "Cabin", sans-serif; 63 | font-size: 20px; 64 | font-weight: 400; 65 | text-transform: uppercase; 66 | /*color: #000;*/ 67 | color: white; 68 | margin-top: 0px; 69 | margin-bottom: 25px; 70 | } 71 | 72 | @media only screen and (max-width: 767px) { 73 | .notfound .notfound-404 { 74 | height: 200px; 75 | } 76 | .notfound .notfound-404 h1 { 77 | font-size: 200px; 78 | } 79 | } 80 | 81 | @media only screen and (max-width: 480px) { 82 | .notfound .notfound-404 { 83 | height: 162px; 84 | } 85 | .notfound .notfound-404 h1 { 86 | font-size: 162px; 87 | height: 150px; 88 | line-height: 162px; 89 | } 90 | .notfound h2 { 91 | font-size: 16px; 92 | } 93 | } 94 | -------------------------------------------------------------------------------- /src/Components/PageNotFound404/PageNotFound404.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import "./PageNotFound404.css"; 3 | 4 | function PageNotFound404() { 5 | return ( 6 |
7 |
8 |
9 |

Oops! Page not found

10 |

11 | 4 12 | 0 13 | 4 14 |

15 |
16 |

we are sorry, but the page you requested was not found

17 |
18 |
19 | ); 20 | } 21 | 22 | export default PageNotFound404; 23 | -------------------------------------------------------------------------------- /src/Components/Room/Room.css: -------------------------------------------------------------------------------- 1 | .room { 2 | width: 100%; 3 | height: 100%; 4 | background-color: #111; 5 | cursor: pointer; 6 | } 7 | 8 | .room__box { 9 | width: 100%; 10 | height: 100%; 11 | } 12 | .room__options { 13 | position: fixed; 14 | bottom: 60px; 15 | transition: all 0.4s; 16 | cursor: pointer; 17 | width: 100%; 18 | padding: 30px; 19 | display: flex; 20 | justify-content: center; 21 | align-items: center; 22 | z-index: 999; 23 | } 24 | .room__optionsContainer { 25 | display: flex; 26 | } 27 | .room__icon { 28 | margin: 10px; 29 | width: 60px; 30 | height: 60px; 31 | display: flex; 32 | align-items: center; 33 | justify-content: center; 34 | border-radius: 50%; 35 | background-color: #5277f0; 36 | transition: all 0.15s; 37 | } 38 | .chatIcon { 39 | position: relative; 40 | } 41 | 42 | .room .room__userVideo { 43 | position: fixed; 44 | z-index: 1; 45 | top: 10px; 46 | right: 10px; 47 | border-radius: 5px; 48 | width: 220px; 49 | height: 120px; 50 | overflow: hidden; 51 | border: solid 0.2px #a0a0a026; 52 | } 53 | 54 | .room .volumeOff__icon { 55 | position: fixed; 56 | top: 15px; 57 | left: 15px; 58 | z-index: 999; 59 | background-color: #00000036; 60 | } 61 | .room .volumeOff__icon:hover { 62 | background-color: #00000046; 63 | } 64 | .room__options .room__optionsContainer .chatIcon .chat__notify { 65 | position: absolute; 66 | top: 5px; 67 | right: 8px; 68 | color: red; 69 | font-size: 1.4rem; 70 | } 71 | .room__icon:hover { 72 | background-color: #345de2; 73 | } 74 | 75 | .room__optionsContainer .MuiSvgIcon-root { 76 | font-size: 2rem; 77 | color: white; 78 | } 79 | .room__optionsContainer .endCall .MuiSvgIcon-root { 80 | color: red; 81 | } 82 | .room__optionsContainer .endCall:hover { 83 | background-color: #f7f7f7; 84 | } 85 | .room__optionsContainer .endCall { 86 | background-color: white; 87 | } 88 | 89 | .room .hide { 90 | bottom: -100%; 91 | } 92 | 93 | @media screen and (max-width: 1000px) { 94 | .room__icon { 95 | width: 58px; 96 | height: 58px; 97 | margin: 8px; 98 | } 99 | .room__optionsContainer .MuiSvgIcon-root { 100 | font-size: 1.8rem; 101 | } 102 | } 103 | @media screen and (max-width: 500px) { 104 | .room__icon { 105 | width: 55px; 106 | height: 55px; 107 | margin: 7px; 108 | } 109 | .room__optionsContainer .MuiSvgIcon-root { 110 | font-size: 1.7rem; 111 | } 112 | .room .room__userVideo { 113 | width: 185px; 114 | height: 100px; 115 | } 116 | .room__options .room__optionsContainer .chatIcon .chat__notify { 117 | font-size: 1.4rem; 118 | right: 7px; 119 | } 120 | } 121 | 122 | @media screen and (max-width: 350px) { 123 | .room__icon { 124 | width: 50px; 125 | height: 50px; 126 | margin: 6px; 127 | } 128 | .room .room__userVideo { 129 | width: 180px; 130 | height: 95px; 131 | } 132 | .room__optionsContainer .MuiSvgIcon-root { 133 | font-size: 1.6rem; 134 | } 135 | .room__options .room__optionsContainer .chatIcon .chat__notify { 136 | font-size: 1.2rem; 137 | right: 5px; 138 | } 139 | } 140 | -------------------------------------------------------------------------------- /src/Components/Room/Room.js: -------------------------------------------------------------------------------- 1 | import React, { useState, useEffect, useCallback } from "react"; 2 | import "./Room.css"; 3 | import Video from "../Video/Video"; 4 | import VideocamIcon from "@material-ui/icons/Videocam"; 5 | import { 6 | CallEnd, 7 | Chat, 8 | FiberManualRecord, 9 | FlipCameraIos, 10 | Mic, 11 | MicOff, 12 | VideocamOff, 13 | VolumeOff, 14 | VolumeUp, 15 | } from "@material-ui/icons"; 16 | import Chats from "../Chats/Chats"; 17 | 18 | function Room({ 19 | myInfo, 20 | userInfo, 21 | endCall, 22 | videoCamOffHandler, 23 | micOffHandler, 24 | switchCamera, 25 | }) { 26 | const [notificationIndicator, setNotificationIndicator] = useState(false); 27 | const [showChats, setShowChat] = useState(false); 28 | const [volumeOff, setVolumeOff] = useState(false); 29 | const [hide, setHide] = useState(false); 30 | const [switchIcon, setSwitchIcon] = useState(false); 31 | 32 | //show red dot on top of chat icon on receiving new messages 33 | const notifyHandler = useCallback(() => { 34 | if (!showChats) { 35 | setNotificationIndicator(true); 36 | } 37 | }, [showChats]); 38 | 39 | //automatically hide icons after some time 40 | useEffect(() => { 41 | const show = () => { 42 | setHide((hide) => !hide); 43 | }; 44 | window.addEventListener("click", show); 45 | let timeOut = null; 46 | if (!hide) { 47 | timeOut = setTimeout(() => { 48 | setHide(true); 49 | }, 10000); 50 | } 51 | return () => { 52 | if (timeOut) { 53 | clearTimeout(timeOut); 54 | } 55 | window.removeEventListener("click", show); 56 | }; 57 | }, [hide]); 58 | 59 | //show icon only in mobile 60 | useEffect(() => { 61 | //no of cameras in a device. 62 | let cameras = 0; 63 | navigator.mediaDevices.enumerateDevices().then((devices) => { 64 | devices.forEach((device) => { 65 | if (device.kind === "videoinput") { 66 | cameras++; 67 | } 68 | if (cameras > 1) { 69 | setSwitchIcon(true); 70 | } 71 | }); 72 | }); 73 | }, []); 74 | 75 | return ( 76 |
77 |
78 |
87 |
88 |
98 |
99 |
100 |
101 | {myInfo.micOff ? : } 102 |
103 |
104 | {myInfo.videoCamOff ? : } 105 |
106 |
107 | 108 |
109 |
{ 112 | setVolumeOff(!volumeOff); 113 | }} 114 | > 115 | {volumeOff ? : } 116 |
117 | 118 | {switchIcon && ( 119 |
123 | 124 |
125 | )} 126 |
{ 129 | setShowChat(!showChats); 130 | setNotificationIndicator(false); 131 | }} 132 | > 133 | 134 | {notificationIndicator && !showChats && ( 135 | 136 | )} 137 |
138 |
139 |
140 | setShowChat(!showChats)} 148 | notifyHandler={notifyHandler} 149 | /> 150 |
151 | ); 152 | } 153 | 154 | export default Room; 155 | -------------------------------------------------------------------------------- /src/Components/Video/Video.css: -------------------------------------------------------------------------------- 1 | .video { 2 | background-color: #1b1b1b; 3 | position: relative; 4 | overflow: hidden; 5 | width: 100%; 6 | height: 100%; 7 | } 8 | video { 9 | width: 100%; 10 | height: 100%; 11 | -o-object-fit: cover; 12 | object-fit: cover; 13 | } 14 | .fullScreen video { 15 | width: 100vw; 16 | height: 100vh; 17 | } 18 | .fullScreen { 19 | width: 100vw; 20 | height: 100vh; 21 | } 22 | .video__stopped { 23 | position: absolute; 24 | top: 0; 25 | left: 0; 26 | bottom: 0; 27 | right: 0; 28 | background-color: #111; 29 | } 30 | 31 | .video__user { 32 | position: absolute; 33 | left: 0px; 34 | bottom: 0px; 35 | font-size: 0.9rem; 36 | background-color: #000000cc; 37 | border-top-right-radius: 4px; 38 | padding: 5px; 39 | display: flex; 40 | z-index: 1; 41 | align-items: center; 42 | max-width: 90%; 43 | border: solid 0.2px #a0a0a026; 44 | } 45 | .video__user .MuiSvgIcon-root { 46 | color: red; 47 | margin-right: 5px; 48 | } 49 | .video__user .video__info { 50 | margin-right: 5px; 51 | } 52 | .video__userName { 53 | padding: 4px; 54 | } 55 | .video__userID { 56 | margin: 5px 0; 57 | word-wrap: break-word; 58 | word-break: break-all; 59 | line-height: 1.2rem; 60 | } 61 | -------------------------------------------------------------------------------- /src/Components/Video/Video.js: -------------------------------------------------------------------------------- 1 | import { MicOff, VideocamOff } from "@material-ui/icons"; 2 | import React, { useEffect, useRef } from "react"; 3 | import "./Video.css"; 4 | import { truncate } from "../../Utils/truncate"; 5 | 6 | function Video({ 7 | stream, 8 | id, 9 | userName, 10 | volumeOff, 11 | videoCamOff, 12 | micOff, 13 | full, 14 | you, 15 | addStyle, 16 | }) { 17 | const Video = useRef(); 18 | 19 | useEffect(() => { 20 | if (videoCamOff) { 21 | if (stream && stream.getVideoTracks().length > 0) { 22 | stream.getVideoTracks().forEach((track) => { 23 | track.enabled = false; 24 | }); 25 | } 26 | } else { 27 | if (stream && stream.getVideoTracks().length > 0) { 28 | stream.getVideoTracks().forEach((track) => { 29 | track.enabled = true; 30 | }); 31 | } 32 | } 33 | if (micOff) { 34 | if (stream && stream.getAudioTracks().length > 0) { 35 | stream.getAudioTracks().forEach((track) => { 36 | track.enabled = false; 37 | }); 38 | } 39 | } else { 40 | if (stream && stream.getAudioTracks().length > 0) { 41 | stream.getAudioTracks().forEach((track) => { 42 | track.enabled = true; 43 | }); 44 | } 45 | } 46 | if (volumeOff) { 47 | Video.current.volume = 0.0; 48 | Video.current.muted = true; 49 | } else { 50 | Video.current.volume = 1.0; 51 | Video.current.muted = false; 52 | } 53 | Video.current.srcObject = stream; 54 | const playVideo = () => { 55 | Video.current.play(); 56 | }; 57 | Video.current.addEventListener("loadedmetadata", playVideo); 58 | 59 | const VideoElement = Video.current; 60 | return () => { 61 | VideoElement.removeEventListener("loadedmetadata", playVideo); 62 | }; 63 | }, [stream, volumeOff, micOff, videoCamOff]); 64 | 65 | return ( 66 |
67 | 72 | {videoCamOff &&
} 73 | {userName && ( 74 |
75 | {!you && ( 76 |
77 | {videoCamOff && } 78 | {micOff && } 79 |
80 | )} 81 |
82 | {truncate(userName, 30)} 83 | {id &&
ID : {id}
} 84 |
85 |
86 | )} 87 |
88 | ); 89 | } 90 | 91 | export default Video; 92 | -------------------------------------------------------------------------------- /src/Images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Pinqua/Call-Buddy-Frontend/f8d2500ccede37adccfb6052e7cc85c8b36ec1fc/src/Images/logo.png -------------------------------------------------------------------------------- /src/Utils/truncate.js: -------------------------------------------------------------------------------- 1 | export function truncate(str, n) { 2 | return str?.length > n ? str.substr(0, n - 1) + "..." : str; 3 | } 4 | -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | -webkit-box-sizing: border-box; 6 | scroll-behavior: smooth; 7 | -webkit-tap-highlight-color: transparent; 8 | } 9 | html { 10 | scroll-behavior: smooth; 11 | } 12 | 13 | body { 14 | margin: 0; 15 | font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Roboto", "Oxygen", 16 | "Ubuntu", "Cantarell", "Fira Sans", "Droid Sans", "Helvetica Neue", 17 | sans-serif; 18 | font-family: "Helvetica Neue", Helvetica, Arial, sans-serif; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | min-width: 320px; 22 | font-weight: 400; 23 | -ms-overflow-style: none; /* IE and Edge */ 24 | scrollbar-width: none; 25 | } 26 | 27 | body::-webkit-scrollbar { 28 | display: none; 29 | } 30 | code { 31 | font-family: source-code-pro, Menlo, Monaco, Consolas, "Courier New", 32 | monospace; 33 | } 34 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import "./index.css"; 4 | import App from "./App"; 5 | import "./normalize.css"; 6 | import * as serviceWorkerRegistration from "./serviceWorkerRegistration"; 7 | import reportWebVitals from "./reportWebVitals"; 8 | 9 | ReactDOM.render( 10 | 11 | 12 | , 13 | document.getElementById("root") 14 | ); 15 | 16 | // If you want your app to work offline and load faster, you can change 17 | // unregister() to register() below. Note this comes with some pitfalls. 18 | // Learn more about service workers: https://cra.link/PWA 19 | serviceWorkerRegistration.register(); 20 | 21 | // If you want to start measuring performance in your app, pass a function 22 | // to log results (for example: reportWebVitals(console.log)) 23 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 24 | reportWebVitals(); 25 | -------------------------------------------------------------------------------- /src/normalize.css: -------------------------------------------------------------------------------- 1 | /*! normalize.css v8.0.1 | MIT License | github.com/necolas/normalize.css */ 2 | 3 | /* Document 4 | ========================================================================== */ 5 | 6 | /** 7 | * 1. Correct the line height in all browsers. 8 | * 2. Prevent adjustments of font size after orientation changes in iOS. 9 | */ 10 | 11 | html { 12 | line-height: 1.15; /* 1 */ 13 | -webkit-text-size-adjust: 100%; /* 2 */ 14 | } 15 | 16 | /* Sections 17 | ========================================================================== */ 18 | 19 | /** 20 | * Remove the margin in all browsers. 21 | */ 22 | 23 | body { 24 | margin: 0; 25 | } 26 | 27 | /** 28 | * Render the `main` element consistently in IE. 29 | */ 30 | 31 | main { 32 | display: block; 33 | } 34 | 35 | /** 36 | * Correct the font size and margin on `h1` elements within `section` and 37 | * `article` contexts in Chrome, Firefox, and Safari. 38 | */ 39 | 40 | h1 { 41 | font-size: 2em; 42 | margin: 0.67em 0; 43 | } 44 | 45 | /* Grouping content 46 | ========================================================================== */ 47 | 48 | /** 49 | * 1. Add the correct box sizing in Firefox. 50 | * 2. Show the overflow in Edge and IE. 51 | */ 52 | 53 | hr { 54 | box-sizing: content-box; /* 1 */ 55 | height: 0; /* 1 */ 56 | overflow: visible; /* 2 */ 57 | } 58 | 59 | /** 60 | * 1. Correct the inheritance and scaling of font size in all browsers. 61 | * 2. Correct the odd `em` font sizing in all browsers. 62 | */ 63 | 64 | pre { 65 | font-family: monospace, monospace; /* 1 */ 66 | font-size: 1em; /* 2 */ 67 | } 68 | 69 | /* Text-level semantics 70 | ========================================================================== */ 71 | 72 | /** 73 | * Remove the gray background on active links in IE 10. 74 | */ 75 | 76 | a { 77 | background-color: transparent; 78 | } 79 | 80 | /** 81 | * 1. Remove the bottom border in Chrome 57- 82 | * 2. Add the correct text decoration in Chrome, Edge, IE, Opera, and Safari. 83 | */ 84 | 85 | abbr[title] { 86 | border-bottom: none; /* 1 */ 87 | text-decoration: underline; /* 2 */ 88 | text-decoration: underline dotted; /* 2 */ 89 | } 90 | 91 | /** 92 | * Add the correct font weight in Chrome, Edge, and Safari. 93 | */ 94 | 95 | b, 96 | strong { 97 | font-weight: bolder; 98 | } 99 | 100 | /** 101 | * 1. Correct the inheritance and scaling of font size in all browsers. 102 | * 2. Correct the odd `em` font sizing in all browsers. 103 | */ 104 | 105 | code, 106 | kbd, 107 | samp { 108 | font-family: monospace, monospace; /* 1 */ 109 | font-size: 1em; /* 2 */ 110 | } 111 | 112 | /** 113 | * Add the correct font size in all browsers. 114 | */ 115 | 116 | small { 117 | font-size: 80%; 118 | } 119 | 120 | /** 121 | * Prevent `sub` and `sup` elements from affecting the line height in 122 | * all browsers. 123 | */ 124 | 125 | sub, 126 | sup { 127 | font-size: 75%; 128 | line-height: 0; 129 | position: relative; 130 | vertical-align: baseline; 131 | } 132 | 133 | sub { 134 | bottom: -0.25em; 135 | } 136 | 137 | sup { 138 | top: -0.5em; 139 | } 140 | 141 | /* Embedded content 142 | ========================================================================== */ 143 | 144 | /** 145 | * Remove the border on images inside links in IE 10. 146 | */ 147 | 148 | img { 149 | border-style: none; 150 | } 151 | 152 | /* Forms 153 | ========================================================================== */ 154 | 155 | /** 156 | * 1. Change the font styles in all browsers. 157 | * 2. Remove the margin in Firefox and Safari. 158 | */ 159 | 160 | button, 161 | input, 162 | optgroup, 163 | select, 164 | textarea { 165 | font-family: inherit; /* 1 */ 166 | font-size: 100%; /* 1 */ 167 | line-height: 1.15; /* 1 */ 168 | margin: 0; /* 2 */ 169 | } 170 | 171 | /** 172 | * Show the overflow in IE. 173 | * 1. Show the overflow in Edge. 174 | */ 175 | 176 | button, 177 | input { 178 | /* 1 */ 179 | overflow: visible; 180 | } 181 | 182 | /** 183 | * Remove the inheritance of text transform in Edge, Firefox, and IE. 184 | * 1. Remove the inheritance of text transform in Firefox. 185 | */ 186 | 187 | button, 188 | select { 189 | /* 1 */ 190 | text-transform: none; 191 | } 192 | 193 | /** 194 | * Correct the inability to style clickable types in iOS and Safari. 195 | */ 196 | 197 | button, 198 | [type="button"], 199 | [type="reset"], 200 | [type="submit"] { 201 | -webkit-appearance: button; 202 | } 203 | 204 | /** 205 | * Remove the inner border and padding in Firefox. 206 | */ 207 | 208 | button::-moz-focus-inner, 209 | [type="button"]::-moz-focus-inner, 210 | [type="reset"]::-moz-focus-inner, 211 | [type="submit"]::-moz-focus-inner { 212 | border-style: none; 213 | padding: 0; 214 | } 215 | 216 | /** 217 | * Restore the focus styles unset by the previous rule. 218 | */ 219 | 220 | button:-moz-focusring, 221 | [type="button"]:-moz-focusring, 222 | [type="reset"]:-moz-focusring, 223 | [type="submit"]:-moz-focusring { 224 | outline: 1px dotted ButtonText; 225 | } 226 | 227 | /** 228 | * Correct the padding in Firefox. 229 | */ 230 | 231 | fieldset { 232 | padding: 0.35em 0.75em 0.625em; 233 | } 234 | 235 | /** 236 | * 1. Correct the text wrapping in Edge and IE. 237 | * 2. Correct the color inheritance from `fieldset` elements in IE. 238 | * 3. Remove the padding so developers are not caught out when they zero out 239 | * `fieldset` elements in all browsers. 240 | */ 241 | 242 | legend { 243 | box-sizing: border-box; /* 1 */ 244 | color: inherit; /* 2 */ 245 | display: table; /* 1 */ 246 | max-width: 100%; /* 1 */ 247 | padding: 0; /* 3 */ 248 | white-space: normal; /* 1 */ 249 | } 250 | 251 | /** 252 | * Add the correct vertical alignment in Chrome, Firefox, and Opera. 253 | */ 254 | 255 | progress { 256 | vertical-align: baseline; 257 | } 258 | 259 | /** 260 | * Remove the default vertical scrollbar in IE 10+. 261 | */ 262 | 263 | textarea { 264 | overflow: auto; 265 | } 266 | 267 | /** 268 | * 1. Add the correct box sizing in IE 10. 269 | * 2. Remove the padding in IE 10. 270 | */ 271 | 272 | [type="checkbox"], 273 | [type="radio"] { 274 | box-sizing: border-box; /* 1 */ 275 | padding: 0; /* 2 */ 276 | } 277 | 278 | /** 279 | * Correct the cursor style of increment and decrement buttons in Chrome. 280 | */ 281 | 282 | [type="number"]::-webkit-inner-spin-button, 283 | [type="number"]::-webkit-outer-spin-button { 284 | height: auto; 285 | } 286 | 287 | /** 288 | * 1. Correct the odd appearance in Chrome and Safari. 289 | * 2. Correct the outline style in Safari. 290 | */ 291 | 292 | [type="search"] { 293 | -webkit-appearance: textfield; /* 1 */ 294 | outline-offset: -2px; /* 2 */ 295 | } 296 | 297 | /** 298 | * Remove the inner padding in Chrome and Safari on macOS. 299 | */ 300 | 301 | [type="search"]::-webkit-search-decoration { 302 | -webkit-appearance: none; 303 | } 304 | 305 | /** 306 | * 1. Correct the inability to style clickable types in iOS and Safari. 307 | * 2. Change font properties to `inherit` in Safari. 308 | */ 309 | 310 | ::-webkit-file-upload-button { 311 | -webkit-appearance: button; /* 1 */ 312 | font: inherit; /* 2 */ 313 | } 314 | 315 | /* Interactive 316 | ========================================================================== */ 317 | 318 | /* 319 | * Add the correct display in Edge, IE 10+, and Firefox. 320 | */ 321 | 322 | details { 323 | display: block; 324 | } 325 | 326 | /* 327 | * Add the correct display in all browsers. 328 | */ 329 | 330 | summary { 331 | display: list-item; 332 | } 333 | 334 | /* Misc 335 | ========================================================================== */ 336 | 337 | /** 338 | * Add the correct display in IE 10+. 339 | */ 340 | 341 | template { 342 | display: none; 343 | } 344 | 345 | /** 346 | * Add the correct display in IE 10. 347 | */ 348 | 349 | [hidden] { 350 | display: none; 351 | } 352 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = (onPerfEntry) => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import("web-vitals").then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/service-worker.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-globals */ 2 | 3 | // This service worker can be customized! 4 | // See https://developers.google.com/web/tools/workbox/modules 5 | // for the list of available Workbox modules, or add any other 6 | // code you'd like. 7 | // You can also remove this file if you'd prefer not to use a 8 | // service worker, and the Workbox build step will be skipped. 9 | 10 | import { clientsClaim } from "workbox-core"; 11 | import { ExpirationPlugin } from "workbox-expiration"; 12 | import { precacheAndRoute, createHandlerBoundToURL } from "workbox-precaching"; 13 | import { registerRoute } from "workbox-routing"; 14 | import { StaleWhileRevalidate } from "workbox-strategies"; 15 | 16 | clientsClaim(); 17 | 18 | // Precache all of the assets generated by your build process. 19 | // Their URLs are injected into the manifest variable below. 20 | // This variable must be present somewhere in your service worker file, 21 | // even if you decide not to use precaching. See https://cra.link/PWA 22 | precacheAndRoute(self.__WB_MANIFEST); 23 | 24 | // Set up App Shell-style routing, so that all navigation requests 25 | // are fulfilled with your index.html shell. Learn more at 26 | // https://developers.google.com/web/fundamentals/architecture/app-shell 27 | const fileExtensionRegexp = new RegExp("/[^/?]+\\.[^/]+$"); 28 | registerRoute( 29 | // Return false to exempt requests from being fulfilled by index.html. 30 | ({ request, url }) => { 31 | // If this isn't a navigation, skip. 32 | if (request.mode !== "navigate") { 33 | return false; 34 | } // If this is a URL that starts with /_, skip. 35 | 36 | if (url.pathname.startsWith("/_")) { 37 | return false; 38 | } // If this looks like a URL for a resource, because it contains // a file extension, skip. 39 | 40 | if (url.pathname.match(fileExtensionRegexp)) { 41 | return false; 42 | } // Return true to signal that we want to use the handler. 43 | 44 | return true; 45 | }, 46 | createHandlerBoundToURL(process.env.PUBLIC_URL + "/index.html") 47 | ); 48 | 49 | // An example runtime caching route for requests that aren't handled by the 50 | // precache, in this case same-origin .png requests like those from in public/ 51 | registerRoute( 52 | // Add in any other file extensions or routing criteria as needed. 53 | ({ url }) => 54 | url.origin === self.location.origin && url.pathname.endsWith(".png"), // Customize this strategy as needed, e.g., by changing to CacheFirst. 55 | new StaleWhileRevalidate({ 56 | cacheName: "images", 57 | plugins: [ 58 | // Ensure that once this runtime cache reaches a maximum size the 59 | // least-recently used images are removed. 60 | new ExpirationPlugin({ maxEntries: 50 }), 61 | ], 62 | }) 63 | ); 64 | 65 | // This allows the web app to trigger skipWaiting via 66 | // registration.waiting.postMessage({type: 'SKIP_WAITING'}) 67 | self.addEventListener("message", (event) => { 68 | if (event.data && event.data.type === "SKIP_WAITING") { 69 | self.skipWaiting(); 70 | } 71 | }); 72 | 73 | // Any other custom service worker logic can go here. 74 | -------------------------------------------------------------------------------- /src/serviceWorkerRegistration.js: -------------------------------------------------------------------------------- 1 | // This optional code is used to register a service worker. 2 | // register() is not called by default. 3 | 4 | // This lets the app load faster on subsequent visits in production, and gives 5 | // it offline capabilities. However, it also means that developers (and users) 6 | // will only see deployed updates on subsequent visits to a page, after all the 7 | // existing tabs open on the page have been closed, since previously cached 8 | // resources are updated in the background. 9 | 10 | // To learn more about the benefits of this model and instructions on how to 11 | // opt-in, read https://cra.link/PWA 12 | 13 | const isLocalhost = Boolean( 14 | window.location.hostname === "localhost" || 15 | // [::1] is the IPv6 localhost address. 16 | window.location.hostname === "[::1]" || 17 | // 127.0.0.0/8 are considered localhost for IPv4. 18 | window.location.hostname.match( 19 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 20 | ) 21 | ); 22 | 23 | export function register(config) { 24 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 25 | // The URL constructor is available in all browsers that support SW. 26 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location.href); 27 | if (publicUrl.origin !== window.location.origin) { 28 | // Our service worker won't work if PUBLIC_URL is on a different origin 29 | // from what our page is served on. This might happen if a CDN is used to 30 | // serve assets; see https://github.com/facebook/create-react-app/issues/2374 31 | return; 32 | } 33 | 34 | window.addEventListener("load", () => { 35 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 36 | 37 | if (isLocalhost) { 38 | // This is running on localhost. Let's check if a service worker still exists or not. 39 | checkValidServiceWorker(swUrl, config); 40 | 41 | // Add some additional logging to localhost, pointing developers to the 42 | // service worker/PWA documentation. 43 | navigator.serviceWorker.ready.then(() => { 44 | console.log( 45 | "This web app is being served cache-first by a service " + 46 | "worker. To learn more, visit https://cra.link/PWA" 47 | ); 48 | }); 49 | } else { 50 | // Is not localhost. Just register service worker 51 | registerValidSW(swUrl, config); 52 | } 53 | }); 54 | } 55 | } 56 | 57 | function registerValidSW(swUrl, config) { 58 | navigator.serviceWorker 59 | .register(swUrl) 60 | .then((registration) => { 61 | registration.onupdatefound = () => { 62 | const installingWorker = registration.installing; 63 | if (installingWorker == null) { 64 | return; 65 | } 66 | installingWorker.onstatechange = () => { 67 | if (installingWorker.state === "installed") { 68 | if (navigator.serviceWorker.controller) { 69 | // At this point, the updated precached content has been fetched, 70 | // but the previous service worker will still serve the older 71 | // content until all client tabs are closed. 72 | console.log( 73 | "New content is available and will be used when all " + 74 | "tabs for this page are closed. See https://cra.link/PWA." 75 | ); 76 | 77 | // Execute callback 78 | if (config && config.onUpdate) { 79 | config.onUpdate(registration); 80 | } 81 | } else { 82 | // At this point, everything has been precached. 83 | // It's the perfect time to display a 84 | // "Content is cached for offline use." message. 85 | console.log("Content is cached for offline use."); 86 | 87 | // Execute callback 88 | if (config && config.onSuccess) { 89 | config.onSuccess(registration); 90 | } 91 | } 92 | } 93 | }; 94 | }; 95 | }) 96 | .catch((error) => { 97 | console.error("Error during service worker registration:", error); 98 | }); 99 | } 100 | 101 | function checkValidServiceWorker(swUrl, config) { 102 | // Check if the service worker can be found. If it can't reload the page. 103 | fetch(swUrl, { 104 | headers: { "Service-Worker": "script" }, 105 | }) 106 | .then((response) => { 107 | // Ensure service worker exists, and that we really are getting a JS file. 108 | const contentType = response.headers.get("content-type"); 109 | if ( 110 | response.status === 404 || 111 | (contentType != null && contentType.indexOf("javascript") === -1) 112 | ) { 113 | // No service worker found. Probably a different app. Reload the page. 114 | navigator.serviceWorker.ready.then((registration) => { 115 | registration.unregister().then(() => { 116 | window.location.reload(); 117 | }); 118 | }); 119 | } else { 120 | // Service worker found. Proceed as normal. 121 | registerValidSW(swUrl, config); 122 | } 123 | }) 124 | .catch(() => { 125 | console.log( 126 | "No internet connection found. App is running in offline mode." 127 | ); 128 | }); 129 | } 130 | 131 | export function unregister() { 132 | if ("serviceWorker" in navigator) { 133 | navigator.serviceWorker.ready 134 | .then((registration) => { 135 | registration.unregister(); 136 | }) 137 | .catch((error) => { 138 | console.error(error.message); 139 | }); 140 | } 141 | } 142 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import "@testing-library/jest-dom"; 6 | --------------------------------------------------------------------------------