├── .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 |
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 |
--------------------------------------------------------------------------------