├── .gitignore ├── LICENSE ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt └── src ├── App.css ├── App.js ├── App.test.js ├── chat.css ├── index.css ├── index.js ├── pic1.png ├── reportWebVitals.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 | .env 21 | *.backup 22 | 23 | npm-debug.log* 24 | yarn-debug.log* 25 | yarn-error.log* 26 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 M Rizky Satrio 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 | # React WebRTC Chat 2 | 3 | A simple chat application using WebRTC for p2p chat and WebSocket for signaling. 4 | 5 | You can try the live application at: 6 | 7 | https://glowing-creponne-b62856.netlify.app/ 8 | 9 | 10 | ## Features 11 | - E2EE Chat P2P Application 12 | - Using WebRTC and WebSocket 13 | 14 | 15 | ## Build 16 | 17 | - Install nodejs 18 | - Edit the "REACT_APP_SIGNALLING_SERVER" in environment configuration to point to your signaling server (code example in [here](https://github.com/rsatrio/WebRTC-Signaling-Server) ) 19 | - Edit the "REACT_APP_GA_ID" in environment configuration with your Google Analytics Measurement ID 20 | - This client used STUN and Turn Server from OpenRelay Project (https://www.metered.ca/tools/openrelay/). You can change it in the App.js to use your own STUN/TURN server 21 | - Run this to install required NPM and build the application: 22 | 23 | 24 | ```shell 25 | npm install 26 | npm run build 27 | ``` 28 | 29 | ## Explanation 30 | You can find the detail explanation of this application in [this medium blog](https://mrizkysatrio.medium.com/webrtc-chat-application-772539ae97b7). 31 | 32 | 33 | ## Feedback 34 | For feedback, please raise issues in the issue section of the repository. Periodically, I will update the code. Enjoy!!. 35 | 36 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chat-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@emotion/react": "^11.7.1", 7 | "@emotion/styled": "^11.6.0", 8 | "@mui/material": "^5.2.8", 9 | "@testing-library/jest-dom": "^5.16.1", 10 | "@testing-library/react": "^12.1.2", 11 | "@testing-library/user-event": "^13.5.0", 12 | "axios": "^0.24.0", 13 | "bootstrap": "^5.1.3", 14 | "react": "^17.0.2", 15 | "react-bootstrap": "^2.2.3", 16 | "react-dom": "^17.0.2", 17 | "react-ga4": "^1.4.1", 18 | "react-scripts": "5.0.0", 19 | "react-toastify": "^8.2.0", 20 | "web-vitals": "^2.1.3" 21 | }, 22 | "scripts": { 23 | "start": "react-scripts start", 24 | "build": "react-scripts build", 25 | "test": "react-scripts test", 26 | "eject": "react-scripts eject" 27 | }, 28 | "eslintConfig": { 29 | "extends": [ 30 | "react-app", 31 | "react-app/jest" 32 | ] 33 | }, 34 | "browserslist": { 35 | "production": [ 36 | ">0.2%", 37 | "not dead", 38 | "not op_mini all" 39 | ], 40 | "development": [ 41 | "last 1 chrome version", 42 | "last 1 firefox version", 43 | "last 1 safari version" 44 | ] 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsatrio/React-WebRTC-Chat/aefaa6df6a879ce5fb0fc6b64aa3878d904056fb/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | WebRTC Chat Apps 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsatrio/React-WebRTC-Chat/aefaa6df6a879ce5fb0fc6b64aa3878d904056fb/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsatrio/React-WebRTC-Chat/aefaa6df6a879ce5fb0fc6b64aa3878d904056fb/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "WebRTC Chat App", 3 | "name": "WebRTC Chat App", 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 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.css: -------------------------------------------------------------------------------- 1 | .App { 2 | text-align: center; 3 | } 4 | 5 | .App-logo { 6 | height: 40vmin; 7 | pointer-events: none; 8 | } 9 | 10 | @media (prefers-reduced-motion: no-preference) { 11 | .App-logo { 12 | animation: App-logo-spin infinite 20s linear; 13 | } 14 | } 15 | 16 | .App-header { 17 | background-color: #282c34; 18 | min-height: 100vh; 19 | display: flex; 20 | flex-direction: column; 21 | align-items: center; 22 | justify-content: center; 23 | font-size: calc(10px + 2vmin); 24 | color: white; 25 | } 26 | 27 | .App-link { 28 | color: #61dafb; 29 | } 30 | 31 | @keyframes App-logo-spin { 32 | from { 33 | transform: rotate(0deg); 34 | } 35 | to { 36 | transform: rotate(360deg); 37 | } 38 | } 39 | 40 | 41 | @import url('https://fonts.googleapis.com/css?family=Roboto+Mono'); 42 | p.console{font-family: 'Roboto Mono', monospace;} 43 | header.terminal{background:#E0E8F0;height:30px;border-radius:8px 8px 0 0;padding-left:10px;} 44 | .terminal-container header .button{width:12px;height:12px;margin:10px 4px 0 0;display:inline-block;border-radius:8px;}.green{background-color: #3BB662 !important;}.red{background-color: #E75448 !important;} 45 | .yellow{background-color: #E5C30F !important;} 46 | .terminal-container{text-align:left;width:100%;border-radius:10px;margin:auto;margin-bottom:14px;position:relative;} 47 | .terminal-fixed-top{margin-top: 30px;} 48 | .terminal-home{ 49 | background-color: #30353A; 50 | padding: 1.5em 1em 1em 2em; 51 | border-bottom-left-radius: 6px; 52 | border-bottom-right-radius: 6px; 53 | color: #FAFAFA; 54 | } 55 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | 2 | import './App.css'; 3 | import { Button, Card, Grid } from '@mui/material'; 4 | import 'bootstrap/dist/css/bootstrap.min.css'; 5 | import 'bootstrap/dist/js/bootstrap.js'; 6 | 7 | import './chat.css'; 8 | import pic1 from './pic1.png'; 9 | import { ToastContainer, toast } from 'react-toastify'; 10 | import 'react-toastify/dist/ReactToastify.css'; 11 | import ReactGA from "react-ga4"; 12 | 13 | 14 | import { useEffect, useState } from 'react'; 15 | 16 | 17 | var channels = []; 18 | var channelUsers = new Map(); 19 | var friendToName = new Map(); 20 | 21 | 22 | var rtcPeers = new Map(); 23 | var ws1; 24 | var reactGA; 25 | 26 | function App() { 27 | 28 | 29 | const [userId, setUserId] = useState(""); 30 | const [rtcPeers2, setRtcPeers2] = useState(""); 31 | 32 | 33 | var signal1 = { 34 | userId: null, 35 | type: null, 36 | data: null, 37 | toUid: null, 38 | }; 39 | 40 | var chatData = { 41 | userId: null, 42 | data: null, 43 | type: null, 44 | } 45 | 46 | 47 | var userId2 = ""; 48 | 49 | 50 | 51 | useEffect(() => { 52 | ReactGA.initialize(process.env.REACT_APP_GA_ID); 53 | ReactGA.send({ hitType: "pageview", page: "/" }); 54 | document.getElementById('toSend').onkeydown = (event) => { 55 | 56 | 57 | if (event.key == 'Enter') { 58 | event.preventDefault(); 59 | sendChat(); 60 | 61 | } 62 | }; 63 | 64 | 65 | 66 | document.getElementById('toSend').setAttribute('disabled', 'true'); 67 | document.getElementById('btnSend').setAttribute('disabled', 'true'); 68 | document.getElementById('status-online').style.display = 'none'; 69 | document.getElementById('btnShowLogin').click(); 70 | 71 | 72 | }, []); 73 | 74 | function addUserOnChat(pengirim: string, join1: boolean) { 75 | let chatWindow = document.getElementById('chat'); 76 | let child1 = document.createElement("p");; 77 | if (join1) { 78 | child1.className = "text-white text-center m-2 bg-success"; 79 | child1.innerText = pengirim + " joined"; 80 | } 81 | else { 82 | child1.className = "text-white text-center m-2 bg-danger"; 83 | child1.innerText = pengirim + " leaved"; 84 | } 85 | 86 | chatWindow.appendChild(child1); 87 | chatWindow.scrollTop = chatWindow.scrollHeight; 88 | } 89 | 90 | function addChatLine(data1: String, listClass: string, pengirim: string) { 91 | let list = document.createElement('li'); 92 | list.className = listClass; 93 | let entete = document.createElement('div'); 94 | entete.className = 'entete'; 95 | let span1 = document.createElement('span'); 96 | let h2 = document.createElement('h2'); 97 | let h3 = document.createElement('h3'); 98 | h2.innerText = pengirim; 99 | h2.className = "m-2"; 100 | h3.innerText = new Date().toLocaleTimeString(); 101 | entete.appendChild(h3); 102 | entete.appendChild(h2); 103 | 104 | 105 | let triangle = document.createElement('div'); 106 | triangle.className = 'triangle'; 107 | let message = document.createElement('message'); 108 | message.className = 'message'; 109 | message.innerHTML = data1; 110 | list.appendChild(entete); 111 | if (listClass == 'you') { 112 | list.appendChild(triangle); 113 | } 114 | list.appendChild(message); 115 | 116 | let chatWindow = document.getElementById('chat'); 117 | chatWindow.appendChild(list); 118 | chatWindow.scrollTop = chatWindow.scrollHeight; 119 | 120 | 121 | } 122 | 123 | function login() { 124 | let inputName = document.getElementById('myUsername'); 125 | if (inputName.value.length < 1) { 126 | toast.error("Username cannot be blank"); 127 | return; 128 | } 129 | startWebSocket(); 130 | ReactGA.event({action:'login',category:'login'}); 131 | } 132 | 133 | function startWebSocket() { 134 | 135 | // ws1 = new WebSocket('ws://localhost:3030/socket1'); 136 | let socketAddr = process.env.REACT_APP_SIGNALLING_SERVER; 137 | ws1 = new WebSocket(socketAddr); 138 | ws1.onopen = event => { 139 | 140 | signal1.userId = ''; 141 | signal1.type = 'Login'; 142 | signal1.data = ''; 143 | console.log(JSON.stringify(signal1)); 144 | ws1.send(JSON.stringify(signal1)); 145 | 146 | } 147 | 148 | ws1.onerror = (error) => { 149 | toast.error("Error connecting to signalling server, please try login again"); 150 | }; 151 | 152 | ws1.onclose = event => { 153 | document.getElementById('status-offline').style.display = 'block'; 154 | document.getElementById('status-online').style.display = 'none'; 155 | document.getElementById('myUsername').removeAttribute('readOnly'); 156 | let myStatus = document.getElementById('myStatus'); 157 | myStatus.className = 'status orange'; 158 | 159 | document.getElementById('h3-myStatus').innerText = 'offline'; 160 | document.getElementById('h3-myStatus').appendChild(myStatus); 161 | document.getElementById('btnLogin').style.display = 'block'; 162 | 163 | 164 | document.getElementById('toSend').setAttribute('disabled', 'true'); 165 | document.getElementById('btnSend').setAttribute('disabled', 'true'); 166 | } 167 | 168 | ws1.onmessage = event => { 169 | var data1 = JSON.parse(event.data); 170 | console.log(event); 171 | console.log(data1); 172 | console.log(data1.type); 173 | 174 | var data2 = null; 175 | 176 | 177 | console.log("Data from server:" + JSON.stringify(data1)); 178 | if (data1.userId == userId2 || data1.userId.length < 2) { 179 | 180 | return; 181 | } 182 | else if (data1.type == 'NewMember') { 183 | handleNewMemberAndOffer(data1); 184 | } 185 | else if (data1.type == "UserId") { 186 | 187 | setUserId(data1.data); 188 | 189 | userId2 = data1.data; 190 | document.getElementById('status-offline').style.display = 'none'; 191 | document.getElementById('status-online').style.display = 'block'; 192 | document.getElementById('toSend').removeAttribute('disabled'); 193 | document.getElementById('btnSend').removeAttribute('disabled'); 194 | document.getElementById('btnLogin').style.display = 'none'; 195 | document.getElementById('myUsername').setAttribute('readOnly', 'true'); 196 | 197 | let myStatus = document.getElementById('myStatus'); 198 | myStatus.className = 'status green'; 199 | 200 | document.getElementById('h3-myStatus').innerText = 'online'; 201 | document.getElementById('h3-myStatus').appendChild(myStatus); 202 | // document.getElementById('btnShowLogin').click(); 203 | toast.success("Connected to Signalling Server. Please click the Show Login/Chat button"); 204 | newMember(userId2); 205 | 206 | console.log("Set userId2:" + userId2); 207 | // sendOffer(); 208 | } 209 | else if (data1.type == "Offer") { 210 | 211 | data2 = JSON.parse(data1.data); 212 | console.log("Receive offer:" + JSON.stringify(data2)); 213 | handleNewMemberAndOffer(data1); 214 | 215 | } 216 | else if (data1.type == "Ice") { 217 | data2 = JSON.parse(data1.data); 218 | if (data2) { 219 | 220 | let peer1 = rtcPeers.get(data1.userId); 221 | 222 | if (peer1) { 223 | console.log("Tambah Ice Candidate"); 224 | peer1.addIceCandidate(new RTCIceCandidate(data2)).catch(error => { 225 | console.log(data2); 226 | console.log("Error ICE:" + error); 227 | }); 228 | } 229 | } 230 | } 231 | else if (data1.type == "Answer") { 232 | data2 = JSON.parse(data1.data); 233 | let peer1 = rtcPeers.get(data1.userId); 234 | peer1.setRemoteDescription(new RTCSessionDescription(data2)); 235 | 236 | } 237 | 238 | // console.log("Message:"+event.data); 239 | }; 240 | 241 | } 242 | 243 | function errorHandler(error) { 244 | console.log('Error:' + error); 245 | } 246 | 247 | 248 | function addUser(data1: String, friendId: string) { 249 | let newUser = document.createElement("li"); 250 | let img1 = document.createElement("img"); 251 | img1.src = pic1; 252 | img1.width = 55; 253 | img1.height = 55; 254 | let div1 = document.createElement('div'); 255 | let h2 = document.createElement('h2'); 256 | h2.innerText = data1; 257 | let h3 = document.createElement('h3'); 258 | h3.innerText = 'online'; 259 | let span1 = document.createElement('span'); 260 | span1.className = 'status green'; 261 | h3.appendChild(span1); 262 | div1.appendChild(h2); 263 | div1.appendChild(h3); 264 | newUser.appendChild(img1); 265 | newUser.appendChild(div1); 266 | newUser.id = friendId; 267 | 268 | document.getElementById("user-list").appendChild(newUser); 269 | 270 | addUserOnChat(friendToName.get(friendId), true); 271 | } 272 | 273 | function addTerminalLine(data1: String) { 274 | 275 | let new1 = document.createElement("p"); 276 | 277 | new1.classNameName = "console"; 278 | new1.textContent = data1; 279 | // document.getElementById("terminal-home1").appendChild(new1); 280 | } 281 | 282 | function handleNewMemberAndOffer(data1) { 283 | let data3 = JSON.parse(data1.data); 284 | let peerId = data1.userId; 285 | let rtcPeer = new RTCPeerConnection({ 286 | iceServers: [{ 287 | urls: "stun:openrelay.metered.ca:80", 288 | }, 289 | { 290 | urls: "turn:openrelay.metered.ca:80", 291 | username: "openrelayproject", 292 | credential: "openrelayproject", 293 | }, 294 | { 295 | urls: "turn:openrelay.metered.ca:443", 296 | username: "openrelayproject", 297 | credential: "openrelayproject", 298 | }, 299 | ] 300 | }); 301 | console.log('Add peer:' + peerId + "-" + JSON.stringify(rtcPeer)); 302 | 303 | setRtcPeers2(rtcPeers2); 304 | 305 | 306 | 307 | if (data1.type == "NewMember") { 308 | let channel1 = rtcPeer.createDataChannel(Math.floor(Math.random() * 10000000000)); 309 | channelConfig(channel1); 310 | 311 | //create offer 312 | rtcPeer.createOffer().then(a => { 313 | console.log('Sending offer'); 314 | // console.log('UserId:' + userId2); 315 | console.log('UserId:' + userId); 316 | signal1.userId = userId2; 317 | signal1.type = 'Offer'; 318 | signal1.data = JSON.stringify(a); 319 | signal1.toUid = data1.userId; 320 | console.log(JSON.stringify(signal1)); 321 | ws1.send(JSON.stringify(signal1)); 322 | rtcPeer.setLocalDescription(a); 323 | // rtcPeer.currentLocalDescription=offer; 324 | }).then(() => { 325 | 326 | }). 327 | catch(err => { 328 | console.log('Error Offer:' + err); 329 | }); 330 | 331 | 332 | } 333 | //when not new member 334 | else { 335 | let data2 = JSON.parse(data1.data); 336 | console.log('Sending answer'); 337 | rtcPeer.setRemoteDescription(data2).then(() => { 338 | 339 | rtcPeer.createAnswer().then(a => { 340 | 341 | signal1.userId = userId2; 342 | signal1.type = 'Answer'; 343 | signal1.data = JSON.stringify(a); 344 | signal1.toUid = data1.userId; 345 | rtcPeer.setLocalDescription(a); 346 | ws1.send(JSON.stringify(signal1)); 347 | console.log('answer:' + JSON.stringify(a)); 348 | }); 349 | }); 350 | 351 | } 352 | rtcPeer.ondatachannel = event => { 353 | let channel2 = event.channel; 354 | channelConfig(channel2); 355 | } 356 | 357 | 358 | rtcPeer.onicecandidate = event => { 359 | console.log('Got ice candidate'); 360 | if (event.candidate) { 361 | console.log('ice candidate:' + JSON.stringify(event.candidate)); 362 | signal1.userId = userId2; 363 | signal1.type = 'Ice'; 364 | signal1.data = JSON.stringify(event.candidate); 365 | signal1.toUid = data1.userId; 366 | } 367 | console.log('ice candidate2:' + JSON.stringify(event)); 368 | if (event.candidate) { 369 | 370 | if (Object.keys(event.candidate.candidate).length > 1) { 371 | console.log("Kirim candidate ke server"); 372 | ws1.send(JSON.stringify(signal1)); 373 | } 374 | } 375 | }; 376 | rtcPeer.onicecandidateerror = event => { 377 | console.log('ice candidate error'); 378 | // ws1.send(JSON.stringify(event)); 379 | }; 380 | 381 | rtcPeer.onicegatheringstatechange = event => { 382 | console.log('ice gathering'); 383 | 384 | console.log('Ice gathering:' + JSON.stringify(event)); 385 | // ws1.send(JSON.stringify(event)); 386 | }; 387 | 388 | rtcPeer.oniceconnectionstatechange = event => { 389 | console.log('ice connection change'); 390 | // ws1.send(JSON.stringify(event)); 391 | }; 392 | 393 | rtcPeers.set(peerId, rtcPeer); 394 | 395 | } 396 | 397 | function channelConfig(channel1: RTCDataChannel) { 398 | channel1.onclose = event => { 399 | console.log("Close channel:"); 400 | let friendId = channelUsers.get(channel1); 401 | document.getElementById(friendId).remove(); 402 | 403 | addUserOnChat(friendToName.get(friendId), false); 404 | friendToName.delete(friendId); 405 | } 406 | channel1.onmessage = event => { 407 | console.log("Receive msg datachannel:" + event.data); 408 | let dataChat1 = JSON.parse(event.data); 409 | if (dataChat1.type == 'message') { 410 | addChatLine(dataChat1.data, 'you', dataChat1.userId); 411 | } 412 | else { 413 | friendToName.set(dataChat1.userId, dataChat1.data); 414 | addUser(dataChat1.data, dataChat1.userId); 415 | channelUsers.set(channel1, dataChat1.userId); 416 | } 417 | 418 | 419 | 420 | }; 421 | 422 | channel1.onopen = () => { 423 | console.log("Now it's open"); 424 | chatData.userId = userId2; 425 | chatData.type = 'handshake'; 426 | chatData.data = document.getElementById('myUsername').value; 427 | channel1.send(JSON.stringify(chatData)); 428 | } 429 | channels.push(channel1); 430 | 431 | 432 | } 433 | 434 | function updateView() { 435 | // console.log("State" + rtcPeer.connectionState); 436 | // console.log("State" + rtcPeer.iceConnectionState); 437 | // ws1.send("oke1"); 438 | let msg1 = document.getElementById('msg1').value; 439 | 440 | channels.forEach(a => { 441 | console.log('channel is' + a.readyState); 442 | if (a.readyState == 'open') { 443 | a.send(msg1); 444 | } 445 | }); 446 | // channel2.send(msg1); 447 | 448 | // document.getElementById 449 | } 450 | 451 | function sendOffer() { 452 | addUser(document.getElementById('myUsername').value, Math.floor(Math.random() * 100000000)); 453 | addChatLine('TestAja', 'me'); 454 | } 455 | 456 | function newMember(data1: String) { 457 | signal1.userId = data1; 458 | signal1.type = 'NewMember'; 459 | signal1.toUid = 'signaling'; 460 | signal1.data = 'Join'; 461 | ws1.send(JSON.stringify(signal1)); 462 | } 463 | 464 | function sendChat() { 465 | channels.forEach(a => { 466 | let dataToSend = document.getElementById('toSend').value; 467 | chatData.userId = document.getElementById('myUsername').value; 468 | chatData.type = 'message'; 469 | chatData.data = dataToSend; 470 | console.log("Send chat:" + JSON.stringify(chatData)); 471 | if (a.readyState == 'open') { 472 | a.send(JSON.stringify(chatData)); 473 | } 474 | 475 | }); 476 | addChatLine(document.getElementById('toSend').value, 'me', 'Me'); 477 | document.getElementById('toSend').value = ''; 478 | 479 | } 480 | 481 | function clickShowChat() { 482 | 483 | document.getElementById('btnShowLogin').click(); 484 | } 485 | 486 | function disconnectAll() { 487 | ws1.close(); 488 | rtcPeers.forEach((a, b) => { 489 | a.close(); 490 | }); 491 | rtcPeers.clear(); 492 | channels = []; 493 | channelUsers.forEach((a, b) => document.getElementById(a).remove()); 494 | channelUsers.clear(); 495 | ReactGA.event({action:'logout',category:'logout'}); 496 | } 497 | 498 | 499 | const renderMyWeb = () => { 500 | try { 501 | 502 | return ( 503 | 504 |
505 | 506 |
507 |
508 | 511 |
512 |
513 |
514 |
515 |
516 | 548 |
549 |
550 |
551 |
552 |
553 | 554 |
555 |
556 |

You are online

557 |

You are offline

558 |
559 | 560 |
561 | 562 |
    563 | 564 | {/*
  • 565 |
    566 | 567 |

    Vincent

    568 |

    10:12AM, Today

    569 |
    570 |
    571 |
    572 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. 573 |
    574 |
  • */} 575 | {/*
  • 576 |
    577 |

    10:12AM, Today

    578 |

    Vincent

    579 | 580 |
    581 |
    582 |
    583 | Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. 584 |
    585 |
  • */} 586 | 587 | 588 |
589 | 590 | 601 |
602 |
603 |
604 |
605 |
606 | 607 | ) 608 | 609 | } 610 | catch (e) { 611 | console.log('error1:' + e); 612 | return ( 613 |

Error Loading

614 | ) 615 | } 616 | } 617 | 618 | return ( 619 |
620 | 621 | { 622 | renderMyWeb() 623 | } 624 | 625 | 626 |
627 | 628 | ); 629 | } 630 | 631 | export default App; 632 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/chat.css: -------------------------------------------------------------------------------- 1 | *{ 2 | box-sizing:border-box; 3 | } 4 | body{ 5 | background-color:#abd9e9; 6 | font-family:Arial; 7 | } 8 | #container1{ 9 | /* width:100vw; */ 10 | height:100vh; 11 | background:#eff3f7; 12 | margin:0 auto; 13 | font-size:0; 14 | border-radius:5px; 15 | overflow:hidden; 16 | } 17 | aside{ 18 | 19 | height:100vh; 20 | background-color:#3b3e49; 21 | display:inline-block; 22 | font-size:15px; 23 | vertical-align:top; 24 | } 25 | 26 | @media screen and (max-width: 992px) { 27 | aside { 28 | width:25vw; 29 | } 30 | } 31 | 32 | @media screen and (max-width: 900px) and (min-width: 300px) { 33 | aside { 34 | width:90vw; 35 | } 36 | } 37 | 38 | aside ::-webkit-scrollbar { 39 | 40 | background:#3b3e49; 41 | } 42 | main{ 43 | /* width:75vw; */ 44 | height:100vh; 45 | /* display:inline-block; */ 46 | font-size:15px; 47 | vertical-align:top; 48 | } 49 | 50 | aside header{ 51 | padding:30px 20px; 52 | } 53 | aside input{ 54 | width:100%; 55 | height:50px; 56 | line-height:50px; 57 | padding:0 50px 0 20px; 58 | background-color:#5e616a; 59 | border:none; 60 | border-radius:3px; 61 | color:#fff; 62 | 63 | background-repeat:no-repeat; 64 | background-position:170px; 65 | background-size:40px; 66 | } 67 | aside input::placeholder{ 68 | color:#fff; 69 | } 70 | aside ul{ 71 | padding-left:0; 72 | margin:0; 73 | list-style-type:none; 74 | overflow-y:scroll; 75 | height:690px; 76 | } 77 | aside li{ 78 | padding:10px 0; 79 | } 80 | aside li:hover{ 81 | background-color:#5e616a; 82 | } 83 | h2,h3{ 84 | margin:0; 85 | } 86 | aside li img{ 87 | border-radius:50%; 88 | margin-left:20px; 89 | margin-right:8px; 90 | } 91 | aside li div{ 92 | display:inline-block; 93 | vertical-align:top; 94 | margin-top:12px; 95 | } 96 | aside li h2{ 97 | font-size:14px; 98 | color:#fff; 99 | font-weight:normal; 100 | margin-bottom:5px; 101 | } 102 | aside li h3{ 103 | font-size:12px; 104 | color:#7e818a; 105 | font-weight:normal; 106 | } 107 | 108 | .status{ 109 | width:8px; 110 | height:8px; 111 | border-radius:50%; 112 | display:inline-block; 113 | margin-right:7px; 114 | } 115 | .green{ 116 | background-color:#58b666; 117 | } 118 | .orange{ 119 | background-color:#ff725d; 120 | } 121 | .blue{ 122 | background-color:#6fbced; 123 | margin-right:0; 124 | margin-left:7px; 125 | } 126 | 127 | main header{ 128 | height:10px; 129 | padding-top:10px; 130 | } 131 | main header > *{ 132 | display:inline-block; 133 | vertical-align:top; 134 | } 135 | main header img:first-child{ 136 | border-radius:50%; 137 | } 138 | main header img:last-child{ 139 | width:24px; 140 | margin-top:8px; 141 | } 142 | main header div{ 143 | margin-left:10px; 144 | margin-right:145px; 145 | } 146 | main header h2{ 147 | font-size:16px; 148 | margin-bottom:5px; 149 | } 150 | main header h3{ 151 | font-size:14px; 152 | font-weight:normal; 153 | color:#7e818a; 154 | } 155 | 156 | #chat{ 157 | padding-left:0; 158 | margin:30px; 159 | /* margin:0; */ 160 | /* width:50vw; */ 161 | list-style-type:none; 162 | overflow-y:scroll; 163 | height:50vh; 164 | border-top:2px solid #fff; 165 | border-bottom:2px solid #fff; 166 | background-color: #7f90a2; 167 | } 168 | #chat li{ 169 | padding:10px 30px; 170 | } 171 | #chat h2,#chat h3{ 172 | display:inline-block; 173 | font-size:13px; 174 | font-weight:normal; 175 | } 176 | #chat h3{ 177 | color:#bbb; 178 | } 179 | #chat .entete{ 180 | margin-bottom:5px; 181 | } 182 | #chat .message{ 183 | padding:20px; 184 | color:#fff; 185 | line-height:25px; 186 | /* max-width:90%; */ 187 | display:inline-block; 188 | text-align:left; 189 | border-radius:5px; 190 | } 191 | #chat .me{ 192 | text-align:right; 193 | } 194 | #chat .you .message{ 195 | background-color:#58b666; 196 | } 197 | #chat .me .message{ 198 | background-color:#6fbced; 199 | } 200 | #chat .triangle{ 201 | width: 0; 202 | height: 0; 203 | border-style: solid; 204 | border-width: 0 10px 10px 10px; 205 | } 206 | #chat .you .triangle{ 207 | border-color: transparent transparent #58b666 transparent; 208 | margin-left:15px; 209 | } 210 | #chat .me .triangle{ 211 | border-color: transparent transparent #6fbced transparent; 212 | /* margin-left:375px; */ 213 | } 214 | 215 | main footer{ 216 | /* width:80vw; */ 217 | height:25vh; 218 | padding:20px 30px 10px 20px; 219 | } 220 | main footer textarea{ 221 | resize:none; 222 | border:none; 223 | display:block; 224 | width:100%; 225 | height:80px; 226 | border-radius:3px; 227 | padding:20px; 228 | font-size:13px; 229 | margin-bottom:13px; 230 | background-color: #7f90a2; 231 | } 232 | main footer textarea::placeholder{ 233 | color:#ddd; 234 | } 235 | main footer img{ 236 | height:30px; 237 | cursor:pointer; 238 | } 239 | main footer a{ 240 | text-decoration:none; 241 | text-transform:uppercase; 242 | font-weight:bold; 243 | color:#6fbced; 244 | vertical-align:top; 245 | margin-left:333px; 246 | margin-top:5px; 247 | display:inline-block; 248 | } 249 | 250 | #btnSend { 251 | float: right !important; 252 | } 253 | 254 | .footer-07 { 255 | background: #121212; 256 | } -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/pic1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/rsatrio/React-WebRTC-Chat/aefaa6df6a879ce5fb0fc6b64aa3878d904056fb/src/pic1.png -------------------------------------------------------------------------------- /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/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 | --------------------------------------------------------------------------------