├── .eslintcache
├── .gitignore
├── README.md
├── chat.png
├── createRoom.png
├── debug.log
├── joinRoom.png
├── package-lock.json
├── package.json
├── public
├── favicon.ico
├── index.html
├── logo192.png
├── logo512.png
├── manifest.json
└── robots.txt
├── register.png
├── src
├── assets
│ ├── audio
│ │ └── message.mp3
│ └── images
│ │ ├── chat_bg.jpg
│ │ └── chat_bg_v2.jpg
├── components
│ ├── App
│ │ ├── index.js
│ │ ├── style.css
│ │ └── test.js
│ ├── Chat
│ │ ├── index.tsx
│ │ └── style.css
│ ├── ChatFooter
│ │ ├── index.tsx
│ │ └── style.css
│ ├── ChatHeader
│ │ ├── index.tsx
│ │ └── style.css
│ ├── ConfirmationDialog
│ │ ├── index.tsx
│ │ └── style.css
│ ├── GeneralSnackbar
│ │ ├── index.tsx
│ │ └── style.css
│ ├── Login
│ │ ├── index.tsx
│ │ └── style.css
│ ├── NewRoom
│ │ ├── index.tsx
│ │ └── style.css
│ ├── NotFound
│ │ ├── index.tsx
│ │ └── style.css
│ ├── Room
│ │ ├── index.tsx
│ │ └── style.css
│ ├── RoomDetails
│ │ ├── index.tsx
│ │ └── style.css
│ ├── Sidebar
│ │ ├── index.tsx
│ │ └── style.css
│ ├── SidebarHeader
│ │ ├── index.tsx
│ │ └── style.css
│ ├── SidebarRooms
│ │ ├── index.tsx
│ │ └── style.css
│ └── Signup
│ │ ├── index.tsx
│ │ └── style.css
├── constants.ts
├── context
│ ├── ChatContext.ts
│ └── UserContext.tsx
├── index.css
├── index.js
├── react-app-env.d.ts
├── reportWebVitals.js
├── services
│ ├── Axios.ts
│ ├── Http.ts
│ └── SocketService.ts
├── setupTests.js
└── types.ts
└── tsconfig.json
/.eslintcache:
--------------------------------------------------------------------------------
1 | [{"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\index.js":"1","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\reportWebVitals.js":"2","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\constants.ts":"3","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\App\\index.js":"4","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Chat\\index.tsx":"5","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Sidebar\\index.tsx":"6","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Room\\index.tsx":"7","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\services\\SocketService.ts":"8","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\services\\Axios.ts":"9","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Signup\\index.tsx":"10","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Login\\index.tsx":"11","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\NewRoom\\index.tsx":"12","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\SidebarRooms\\index.tsx":"13","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\RoomDetails\\index.tsx":"14","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\NotFound\\index.tsx":"15","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\services\\Http.ts":"16","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\ConfirmationDialog\\index.tsx":"17","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\GeneralSnackbar\\index.tsx":"18","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\SidebarHeader\\index.tsx":"19","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\ChatHeader\\index.tsx":"20","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\ChatFooter\\index.tsx":"21","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\context\\UserContext.tsx":"22","C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\context\\ChatContext.ts":"23"},{"size":460,"mtime":1612342568435,"results":"24","hashOfConfig":"25"},{"size":362,"mtime":499162500000,"results":"26","hashOfConfig":"25"},{"size":429,"mtime":1612774205891,"results":"27","hashOfConfig":"25"},{"size":1448,"mtime":1612522299442,"results":"28","hashOfConfig":"25"},{"size":3180,"mtime":1612775220262,"results":"29","hashOfConfig":"25"},{"size":1300,"mtime":1612857227792,"results":"30","hashOfConfig":"25"},{"size":6471,"mtime":1612857227312,"results":"31","hashOfConfig":"25"},{"size":1522,"mtime":1612855515246,"results":"32","hashOfConfig":"25"},{"size":425,"mtime":1612855517314,"results":"33","hashOfConfig":"25"},{"size":3630,"mtime":1612455228339,"results":"34","hashOfConfig":"25"},{"size":2661,"mtime":1612518288575,"results":"35","hashOfConfig":"25"},{"size":2656,"mtime":1612514519778,"results":"36","hashOfConfig":"25"},{"size":1205,"mtime":1612857226673,"results":"37","hashOfConfig":"25"},{"size":3345,"mtime":1612780898853,"results":"38","hashOfConfig":"25"},{"size":192,"mtime":1612357024513,"results":"39","hashOfConfig":"25"},{"size":3130,"mtime":1612358494431,"results":"40","hashOfConfig":"25"},{"size":1057,"mtime":1612356813881,"results":"41","hashOfConfig":"25"},{"size":978,"mtime":1612519403824,"results":"42","hashOfConfig":"25"},{"size":2172,"mtime":1612514609310,"results":"43","hashOfConfig":"25"},{"size":934,"mtime":1612505661063,"results":"44","hashOfConfig":"25"},{"size":1287,"mtime":1612514653602,"results":"45","hashOfConfig":"25"},{"size":430,"mtime":1612518354628,"results":"46","hashOfConfig":"25"},{"size":268,"mtime":1612520927894,"results":"47","hashOfConfig":"25"},{"filePath":"48","messages":"49","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"50"},"ijghcb",{"filePath":"51","messages":"52","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"50"},{"filePath":"53","messages":"54","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"56","messages":"57","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"50"},{"filePath":"58","messages":"59","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"60","messages":"61","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"62","messages":"63","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"64","messages":"65","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"66","messages":"67","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"68","messages":"69","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"70","messages":"71","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"72","messages":"73","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"74","messages":"75","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0},{"filePath":"76","messages":"77","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"78","messages":"79","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"80","messages":"81","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"82","messages":"83","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"84","messages":"85","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"86","messages":"87","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"88","messages":"89","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"90","messages":"91","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"92","messages":"93","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},{"filePath":"94","messages":"95","errorCount":0,"warningCount":0,"fixableErrorCount":0,"fixableWarningCount":0,"usedDeprecatedRules":"55"},"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\index.js",[],["96","97"],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\reportWebVitals.js",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\constants.ts",[],["98","99"],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\App\\index.js",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Chat\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Sidebar\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Room\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\services\\SocketService.ts",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\services\\Axios.ts",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Signup\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\Login\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\NewRoom\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\SidebarRooms\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\RoomDetails\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\NotFound\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\services\\Http.ts",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\ConfirmationDialog\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\GeneralSnackbar\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\SidebarHeader\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\ChatHeader\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\components\\ChatFooter\\index.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\context\\UserContext.tsx",[],"C:\\Users\\roseb\\Documents\\Personal\\chat-frontend\\src\\context\\ChatContext.ts",[],{"ruleId":"100","replacedBy":"101"},{"ruleId":"102","replacedBy":"103"},{"ruleId":"100","replacedBy":"101"},{"ruleId":"102","replacedBy":"103"},"no-native-reassign",["104"],"no-negated-in-lhs",["105"],"no-global-assign","no-unsafe-negation"]
--------------------------------------------------------------------------------
/.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 | .vscode
22 | npm-debug.log*
23 | yarn-debug.log*
24 | yarn-error.log*
25 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [![Netlify Status][netlify-shield]][netlify-url]
2 |
3 |
4 |
5 |
Realtime Chat App
6 |
7 |
8 | A web-based messaging application that delivers messages instantaneously.
9 |
10 | View Live Demo »
11 |
12 | View Video Demo »
13 |
14 | Report Bug
15 | ·
16 | Request Feature
17 |
18 |
19 |
20 | Table of Contents
21 |
22 | -
23 | About The Project
24 |
27 |
28 | -
29 | Getting Started
30 |
34 |
35 | - Usage
36 | - Implementation Pipeline
37 | - Contact
38 |
39 |
40 | ## About The Project
41 |
42 | ### Built With
43 |
44 | - **[React](https://reactjs.org/)**
45 | - **[Socket.io](https://socket.io/)**
46 | - **[Typescript](https://www.typescriptlang.org/)**
47 | - [Node.js](https://nodejs.org/en/)
48 | - [MongoDB](https://www.mongodb.com/)
49 |
50 | ## Getting Started
51 |
52 | To get a local copy up and running follow these simple steps.
53 |
54 | ### Prerequisites
55 |
56 | Install latest version of npm
57 |
58 | - npm
59 | ```sh
60 | npm install npm@latest -g
61 | ```
62 |
63 | ### Installation
64 |
65 | 1. Clone the project
66 | ```sh
67 | git clone https://github.com/crookedfingerworks/chat-frontend.git
68 | ```
69 | 2. Go to project directory and Install NPM packages
70 | ```sh
71 | npm install
72 | ```
73 | 3. Create .env file with the ff. content
74 | ```sh
75 | REACT_APP_SERVER_URL=https://rose-chat-backend.herokuapp.com
76 | ```
77 | 4. Start the application
78 | ```sh
79 | npm start
80 | ```
81 |
82 | ## Usage
83 |
84 | **Creating an Account**
85 |
86 | 
87 |
88 | 1. In the login page, click 'Register here'.
89 | 2. Input the necessary fields. Don't worry. It won't take long.
90 | 3. You'll be redirected to the login page. Enter your newly created credentials.
91 |
92 | **Creating a Room**
93 |
94 | 
95 |
96 | 1. Click the message icon on the sidebar header.
97 | 2. Input the necessary fields and proceed.
98 | 3. Share the randomly-generated room code with people you want to invite in the room.
99 |
100 | **Joining a Room**
101 |
102 | 
103 |
104 | 1. Obtain the room code from the room creator.
105 | 2. Click the message icon on the sidebar header.
106 | 3. Click 'Join Room' tab option.
107 | 4. Input room code and proceed.
108 |
109 | ## Implementation Pipeline
110 |
111 |
112 | - Upload Group Image
113 | - "User is typing" indicator
114 | - Emoticons
115 |
116 |
117 | ## Contact
118 |
119 | crooked.finger.works@gmail.com
120 |
121 | Project Link: [https://github.com/crookedfingerworks/chat-frontend](https://github.com/crookedfingerworks/chat-frontend)
122 |
123 | [netlify-shield]: https://img.shields.io/netlify/24e36167-88a7-4e1e-93f5-0986aa1c1b7d?style=for-the-badge
124 | [netlify-url]: https://app.netlify.com/sites/rose-chat-client/deploys
125 |
--------------------------------------------------------------------------------
/chat.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/chat.png
--------------------------------------------------------------------------------
/createRoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/createRoom.png
--------------------------------------------------------------------------------
/debug.log:
--------------------------------------------------------------------------------
1 | [0103/152954.377:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
2 | [0105/160917.925:ERROR:directory_reader_win.cc(43)] FindFirstFile: The system cannot find the path specified. (0x3)
3 |
--------------------------------------------------------------------------------
/joinRoom.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/joinRoom.png
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "chat-frontend",
3 | "version": "0.1.0",
4 | "private": true,
5 | "dependencies": {
6 | "@material-ui/core": "^4.11.3",
7 | "@material-ui/icons": "^4.11.2",
8 | "@testing-library/jest-dom": "^5.11.9",
9 | "@testing-library/react": "^11.2.5",
10 | "@testing-library/user-event": "^12.6.3",
11 | "@types/react-router-dom": "^5.1.7",
12 | "@types/socket.io-client": "^1.4.35",
13 | "@types/styled-components": "^5.1.7",
14 | "axios": "^0.21.1",
15 | "date-fns": "^2.17.0",
16 | "react": "^17.0.1",
17 | "react-debounce-input": "^3.2.3",
18 | "react-dom": "^17.0.1",
19 | "react-router-dom": "^5.2.0",
20 | "react-scripts": "4.0.1",
21 | "react-scrollbars-custom": "^4.0.25",
22 | "rxjs": "^6.6.3",
23 | "socket.io-client": "3.1.1",
24 | "styled-components": "^5.2.1",
25 | "typescript": "^4.1.3",
26 | "web-vitals": "^0.2.4"
27 | },
28 | "scripts": {
29 | "start": "react-scripts start",
30 | "build": "react-scripts build",
31 | "test": "react-scripts test",
32 | "eject": "react-scripts eject"
33 | },
34 | "eslintConfig": {
35 | "extends": [
36 | "react-app",
37 | "react-app/jest"
38 | ]
39 | },
40 | "browserslist": {
41 | "production": [
42 | ">0.2%",
43 | "not dead",
44 | "not op_mini all"
45 | ],
46 | "development": [
47 | "last 1 chrome version",
48 | "last 1 firefox version",
49 | "last 1 safari version"
50 | ]
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/public/favicon.ico:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/public/favicon.ico
--------------------------------------------------------------------------------
/public/index.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 |
12 |
13 |
17 |
18 |
27 |
28 |
29 |
30 | React App
31 |
32 |
33 |
34 |
35 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/public/logo192.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/public/logo192.png
--------------------------------------------------------------------------------
/public/logo512.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/public/logo512.png
--------------------------------------------------------------------------------
/public/manifest.json:
--------------------------------------------------------------------------------
1 | {
2 | "short_name": "React App",
3 | "name": "Create React App Sample",
4 | "icons": [
5 | {
6 | "src": "favicon.ico",
7 | "sizes": "64x64 32x32 24x24 16x16",
8 | "type": "image/x-icon"
9 | },
10 | {
11 | "src": "logo192.png",
12 | "type": "image/png",
13 | "sizes": "192x192"
14 | },
15 | {
16 | "src": "logo512.png",
17 | "type": "image/png",
18 | "sizes": "512x512"
19 | }
20 | ],
21 | "start_url": ".",
22 | "display": "standalone",
23 | "theme_color": "#000000",
24 | "background_color": "#ffffff"
25 | }
26 |
--------------------------------------------------------------------------------
/public/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/register.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/register.png
--------------------------------------------------------------------------------
/src/assets/audio/message.mp3:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/src/assets/audio/message.mp3
--------------------------------------------------------------------------------
/src/assets/images/chat_bg.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/src/assets/images/chat_bg.jpg
--------------------------------------------------------------------------------
/src/assets/images/chat_bg_v2.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/src/assets/images/chat_bg_v2.jpg
--------------------------------------------------------------------------------
/src/components/App/index.js:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';
3 | import './style.css';
4 | import Room from '../Room';
5 | import SignUp from '../Signup';
6 | import Login from '../Login';
7 | import NotFound from '../NotFound';
8 | import { SocketService } from '../../services/SocketService';
9 | import { USER_INITIAL_VALUE } from '../../constants';
10 | import { UserContext } from '../../context/UserContext';
11 | import { ChatContext } from '../../context/ChatContext';
12 | import { StylesProvider } from '@material-ui/core/styles';
13 |
14 | const routes = [
15 | { path: '/signup', component: SignUp },
16 | { path: '/login', component: Login },
17 | { path: '/room', component: Room },
18 | { path: '/', component: Login }
19 | ];
20 |
21 | const chat = new SocketService();
22 |
23 | function App() {
24 | const userJSON = localStorage.getItem('chat-app-user');
25 | const [ userDetails, setUserDetails ] = useState(userJSON !== null ? JSON.parse(userJSON) : USER_INITIAL_VALUE);
26 |
27 | return (
28 |
29 |
30 |
31 |
32 |
33 |
34 | {routes.map(({ path, component }) => )}
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 | );
43 | }
44 |
45 | export default App;
46 |
--------------------------------------------------------------------------------
/src/components/App/style.css:
--------------------------------------------------------------------------------
1 | .app {
2 | height: 100vh;
3 | }
4 | .MuiSvgIcon-root {
5 | font-size: 1.25rem;
6 | }
7 | .MuiAvatar-root.avatar--large {
8 | width: 6.5rem;
9 | height: 6.5rem;
10 | }
11 | .MuiAvatar-colorDefault {
12 | color: #fff;
13 | background-color: #b3b3b3;
14 | }
15 | .MuiButton-contained {
16 | background-color: #023047;
17 | border-radius: 0.75rem;
18 | font-size: 0.8rem;
19 | letter-spacing: 0.15rem;
20 | margin: 1.75rem auto 0;
21 | padding: 0.75rem 5rem;
22 | }
23 | .MuiButton-contained:hover {
24 | background-color: #002538;
25 | }
26 | .MuiButton-contained.secondary {
27 | background-color: #219ebc;
28 | }
29 | .MuiButton-contained.secondary:hover {
30 | background-color: #1e8fac;
31 | }
32 | input,
33 | textarea {
34 | border-radius: 0.75rem;
35 | padding: 0.8rem;
36 | border: 1px solid #dadada;
37 | outline: none;
38 | }
39 | .auth__wrapper {
40 | height: 100vh;
41 | display: flex;
42 | place-items: center;
43 | justify-content: center;
44 | background-color: #023047;
45 | }
46 | .auth__wrapper .area__wrapper {
47 | background-color: #f1faee;
48 | text-align: center;
49 | padding: 5rem 3rem;
50 | width: 25rem;
51 | min-height: 15rem;
52 | border-radius: 0.75rem;
53 | box-shadow: 0 3px 6px rgba(0, 0, 0, 0.16), 0 3px 6px rgba(0, 0, 0, 0.23);
54 | }
55 | .auth__wrapper .area__wrapper > form {
56 | display: flex;
57 | flex-flow: row wrap;
58 | }
59 | .error__msg {
60 | margin: 0 1rem;
61 | font-size: 0.75rem;
62 | color: #a90b0b;
63 | }
64 |
65 | .MuiPaper-root {
66 | border-radius: 0.75rem;
67 | }
68 | .MuiDialogTitle-root,
69 | .MuiDialogContent-root {
70 | background-color: #f1faee;
71 | }
72 | .MuiButton-textPrimary {
73 | color: #002538;
74 | }
75 | .MuiSnackbarContent-root {
76 | background-color: #219ebc;
77 | }
78 |
--------------------------------------------------------------------------------
/src/components/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/components/Chat/index.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar } from '@material-ui/core';
2 | import React, { useState, useEffect, useCallback } from 'react';
3 | import Scrollbar from 'react-scrollbars-custom';
4 | import './style.css';
5 | import { MessagePopulated } from '../../types';
6 | import chatHttp from '../../services/Http';
7 | import { parseISO, differenceInCalendarDays, format, formatDistanceToNow } from 'date-fns';
8 | import PersonIcon from '@material-ui/icons/Person';
9 | import { useChat } from '../../context/ChatContext';
10 | import { useUser } from '../../context/UserContext';
11 | import ChatHeader from '../ChatHeader';
12 | import ChatFooter from '../ChatFooter';
13 |
14 | export interface ChatProps {
15 | roomCode: string;
16 | }
17 |
18 | const Chat = ({ roomCode }: ChatProps) => {
19 | const [ messages, setMessages ] = useState([] as MessagePopulated[]);
20 | const chatSocket = useChat();
21 | const { userDetails } = useUser();
22 | const setRef = useCallback((node) => {
23 | if (node) {
24 | node.scrollIntoView({ smooth: true });
25 | }
26 | }, []);
27 |
28 | useEffect(
29 | () => {
30 | if (chatSocket === null) return;
31 | const subscription = chatSocket.onMessage().subscribe(({ newMsg, updatedRoom }) => {
32 | if (newMsg.roomCode === roomCode) {
33 | setMessages((prevMsgs) => [ ...prevMsgs, newMsg ]);
34 | }
35 | });
36 | return () => {
37 | subscription.unsubscribe();
38 | };
39 | },
40 | [ chatSocket, roomCode ]
41 | );
42 |
43 | useEffect(
44 | () => {
45 | chatHttp
46 | .getMessages({ roomCode })
47 | .then(({ data }) => {
48 | setMessages((prevMsgs) => data.messages);
49 | })
50 | .catch(({ response }) => {
51 | console.log(response.data);
52 | });
53 | },
54 | [ roomCode ]
55 | );
56 |
57 | const formatDate = (date: Date) => {
58 | return differenceInCalendarDays(new Date(), date) > 2
59 | ? format(date, 'EEE MMM d h:m b')
60 | : formatDistanceToNow(date, { addSuffix: true });
61 | };
62 |
63 | return (
64 |
65 |
66 |
67 |
68 |
69 | {messages.map(({ content, user, createdAt }, i) => {
70 | const lastMessage = messages.length - 1 === i;
71 | return (
72 |
77 |
78 |
79 | {user.firstName && user.lastName ? (
80 | user.firstName.charAt(0) + user.lastName.charAt(0)
81 | ) : (
82 |
83 | )}
84 |
85 |
86 |
87 | {userDetails.username === user.username ? 'You' : user.username}
88 |
89 | {content}
90 |
91 |
92 |
{formatDate(parseISO(createdAt))}
93 |
94 | );
95 | })}
96 |
97 |
98 |
99 |
100 |
101 | );
102 | };
103 |
104 | export default Chat;
105 |
--------------------------------------------------------------------------------
/src/components/Chat/style.css:
--------------------------------------------------------------------------------
1 | .chat {
2 | flex-grow: 10;
3 | }
4 | .chat__body {
5 | background-color: #f1faee;
6 | height: calc(100vh - 7.3rem);
7 | }
8 | .chat__main {
9 | padding: 0 1rem 1.75rem;
10 | }
11 | .chat__block {
12 | margin-top: 1rem;
13 | }
14 | .message__block {
15 | display: flex;
16 | }
17 | .message__block .MuiAvatar-root {
18 | margin-right: 0.5rem;
19 | margin-top: 0.2rem;
20 | }
21 | .message__block .chat__person {
22 | font-size: small;
23 | display: block;
24 | margin-bottom: 0.25rem;
25 | font-weight: 600;
26 | letter-spacing: 0.03rem;
27 | }
28 | .chat__message {
29 | position: relative;
30 | width: fit-content;
31 | background-color: #219ebc;
32 | color: #fff;
33 | padding: 0.75rem 1rem;
34 | border-radius: 0.75rem;
35 | margin: 0;
36 | box-shadow: 0 1px 1px 0 rgba(0, 0, 0, 0.14), 0 2px 1px -1px rgba(0, 0, 0, 0.12), 0 1px 3px 0 rgba(0, 0, 0, 0.2);
37 | }
38 | .chat__timestamp {
39 | font-size: x-small;
40 | margin-left: 3.75rem;
41 | color: gray;
42 | }
43 | .chat__block--sender .MuiAvatar-root {
44 | order: 1;
45 | margin-right: 0;
46 | margin-left: 0.5rem;
47 | }
48 | .chat__block--sender .chat__message {
49 | margin-left: auto;
50 | background-color: #8ecae6;
51 | }
52 | .chat__block--sender .chat__timestamp {
53 | display: inline-block;
54 | text-align: right;
55 | margin-left: 0;
56 | margin-right: 3.75rem;
57 | width: calc(100% - 3.75rem);
58 | }
59 |
--------------------------------------------------------------------------------
/src/components/ChatFooter/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton } from '@material-ui/core';
2 | import React, { useState } from 'react';
3 | import { useChat } from '../../context/ChatContext';
4 | import SendIcon from '@material-ui/icons/Send';
5 | import { ChatMessage, User } from '../../types';
6 | import './style.css';
7 |
8 | export interface ChatFooterProps {
9 | roomCode: string;
10 | loggedInUser: User;
11 | }
12 | function ChatFooter({ roomCode, loggedInUser }: ChatFooterProps) {
13 | const [ input, setInput ] = useState('');
14 | const chatSocket = useChat();
15 | const sendMessage = async (e: React.MouseEvent) => {
16 | e.preventDefault();
17 | if (input) {
18 | const messageDetails: ChatMessage = {
19 | userRoom: {
20 | name: loggedInUser.username,
21 | room: roomCode
22 | },
23 | content: input
24 | };
25 | setInput('');
26 |
27 | console.log('sending message: ' + JSON.stringify(messageDetails));
28 | chatSocket.send(messageDetails);
29 | }
30 | };
31 | return (
32 |
33 |
39 |
40 |
41 |
42 |
43 | );
44 | }
45 |
46 | export default ChatFooter;
47 |
--------------------------------------------------------------------------------
/src/components/ChatFooter/style.css:
--------------------------------------------------------------------------------
1 | .chat__footer {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | justify-content: space-between;
5 | align-items: center;
6 | height: 3.5rem;
7 | border-top: 1px solid #cecece;
8 | background-color: #fff;
9 | }
10 | .chat__footer > form {
11 | flex: 1;
12 | display: flex;
13 | margin-left: 0.5rem;
14 | }
15 | .chat__footer > form > input {
16 | flex: 1;
17 | }
18 | .chat__footer > form > button {
19 | display: none;
20 | }
21 | .chat__footer > .MuiButtonBase-root {
22 | margin: -0.25rem 0.25rem 0;
23 | }
24 | .chat__footer .MuiButtonBase-root .MuiSvgIcon-root {
25 | transform: rotate(-45deg);
26 | }
27 |
--------------------------------------------------------------------------------
/src/components/ChatHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import { parseISO } from 'date-fns';
3 | import { MessagePopulated } from '../../types';
4 | import './style.css';
5 |
6 | export interface ChatHeaderProps {
7 | roomCode: string;
8 | messages: MessagePopulated[];
9 | formatDate: (date: Date) => string;
10 | }
11 |
12 | function ChatHeader({ roomCode, messages, formatDate }: ChatHeaderProps) {
13 | return (
14 |
15 |
16 |
Room {roomCode}
17 |
18 | {messages.length > 0 ? (
19 | 'Last activity ' + formatDate(parseISO(messages[messages.length - 1].createdAt))
20 | ) : (
21 | 'No recent activities...'
22 | )}
23 |
24 |
25 |
26 | {/* TODO future implementation */}
27 | {/*
28 |
29 |
30 |
31 |
32 | */}
33 |
34 |
35 | );
36 | }
37 |
38 | export default ChatHeader;
39 |
--------------------------------------------------------------------------------
/src/components/ChatHeader/style.css:
--------------------------------------------------------------------------------
1 | .chat__header {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | align-items: center;
5 | justify-content: space-between;
6 | padding: 0.35rem;
7 | border-bottom: 1px solid #cecece;
8 | background-color: #fff;
9 | }
10 | .chat__headerInfo {
11 | flex: 1;
12 | padding-left: 1rem;
13 | }
14 | .chat__headerInfo > h3 {
15 | font-weight: 700;
16 | margin: 0;
17 | }
18 | .chat__headerInfo > p {
19 | color: gray;
20 | margin: 0 0 0.35rem;
21 | font-size: 0.8rem;
22 | }
23 | .chat__headerIcons > .MuiIconButton-root {
24 | margin-right: 0.5rem;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/ConfirmationDialog/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button, Dialog, DialogActions, DialogContent, DialogContentText, DialogTitle } from '@material-ui/core';
2 | import React from 'react';
3 | import './style.css';
4 |
5 | export interface ConfirmationDialogProps {
6 | content: string;
7 | open: boolean;
8 | onClose: (willProceed: boolean) => void;
9 | }
10 |
11 | function ConfirmationDialog({ content, open, onClose }: ConfirmationDialogProps) {
12 | return (
13 |
32 | );
33 | }
34 |
35 | export default ConfirmationDialog;
36 |
--------------------------------------------------------------------------------
/src/components/ConfirmationDialog/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/src/components/ConfirmationDialog/style.css
--------------------------------------------------------------------------------
/src/components/GeneralSnackbar/index.tsx:
--------------------------------------------------------------------------------
1 | import { IconButton, Snackbar } from '@material-ui/core';
2 | import CloseIcon from '@material-ui/icons/Close';
3 | import React from 'react';
4 | import './style.css';
5 |
6 | export interface GeneralSnackbarProps {
7 | message: string;
8 | open: boolean;
9 | onClose: () => void;
10 | }
11 |
12 | function GeneralSnackbar({ message, open, onClose }: GeneralSnackbarProps) {
13 | const handleClose = (event: React.SyntheticEvent | React.MouseEvent, reason?: string) => {
14 | if (reason === 'clickaway') {
15 | return;
16 | }
17 | onClose();
18 | };
19 |
20 | return (
21 |
22 |
33 |
34 |
35 | }
36 | />
37 |
38 | );
39 | }
40 |
41 | export default GeneralSnackbar;
42 |
--------------------------------------------------------------------------------
/src/components/GeneralSnackbar/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/src/components/GeneralSnackbar/style.css
--------------------------------------------------------------------------------
/src/components/Login/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useEffect, useRef } from 'react';
2 | import { Button } from '@material-ui/core';
3 | import chatHttp from '../../services/Http';
4 | import './style.css';
5 | import { useUser } from '../../context/UserContext';
6 | import { useHistory } from 'react-router-dom';
7 |
8 | export interface LoginProps {
9 | history: ReturnType;
10 | }
11 | function Login({ history }: LoginProps) {
12 | const usernameRef = useRef(null);
13 | const passwordRef = useRef(null);
14 | const [ errorMsg, setErrorMsg ] = useState('');
15 | const { userDetails, setUserDetails } = useUser();
16 |
17 | const proceed = async (e: React.MouseEvent) => {
18 | e.preventDefault();
19 | if (usernameRef.current && usernameRef.current.value && passwordRef.current && passwordRef.current.value) {
20 | chatHttp
21 | .login({ username: usernameRef.current.value, password: passwordRef.current.value })
22 | .then(({ authorization, data }) => {
23 | setErrorMsg('');
24 | localStorage.setItem('chat-app-auth', authorization);
25 | localStorage.setItem('chat-app-user', JSON.stringify(data.userDetails));
26 | setUserDetails(data.userDetails);
27 | history.push('/room');
28 | })
29 | .catch(({ response }) => {
30 | console.log(response.data);
31 | setErrorMsg(response.data.message);
32 | });
33 | } else {
34 | setErrorMsg('Fill-in both username and password');
35 | }
36 | };
37 |
38 | const goToSignup = async () => {
39 | history.push('/signup');
40 | };
41 |
42 | useEffect(() => {
43 | if (usernameRef.current) usernameRef.current.focus();
44 | }, []);
45 |
46 | useEffect(
47 | () => {
48 | const token = localStorage.getItem('chat-app-auth');
49 | if (token && userDetails.username) {
50 | chatHttp.changeLoginStatus({ newValue: true });
51 | history.push('/room');
52 | }
53 | },
54 | [ history, userDetails ]
55 | );
56 |
57 | return (
58 |
59 |
60 |
REALTIME CHAT
61 |
by Rose Bilag
62 |
81 | {errorMsg &&
{errorMsg}}
82 |
83 |
84 | );
85 | }
86 |
87 | export default Login;
88 |
--------------------------------------------------------------------------------
/src/components/Login/style.css:
--------------------------------------------------------------------------------
1 | .login__area h1 {
2 | margin: 0;
3 | }
4 | .login__area > p {
5 | margin: 0;
6 | color: gray;
7 | font-weight: 500;
8 | }
9 | .login__area > form {
10 | margin-top: 2rem;
11 | }
12 | .login__area > form input {
13 | margin: 0.35rem 0;
14 | width: calc(100% - 2rem);
15 | }
16 | .login__area form p {
17 | margin: 1rem auto;
18 | font-size: 0.9rem;
19 | }
20 | .login__area .signup__link {
21 | cursor: pointer;
22 | margin: 0.25rem;
23 | font-weight: 600;
24 | -webkit-transition: .25s ease-out;
25 | -moz-transition: .25s ease-out;
26 | -ms-transition: .25s ease-out;
27 | -o-transition: .25s ease-out;
28 | transition: .25s ease-out;
29 | }
30 | .login__area .signup__link:hover {
31 | color: #219ebc;
32 | }
33 |
--------------------------------------------------------------------------------
/src/components/NewRoom/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import { Button, ButtonGroup, Dialog, DialogTitle, DialogContent } from '@material-ui/core';
3 | import chatHttp from '../../services/Http';
4 | import './style.css';
5 | import { useChat } from '../../context/ChatContext';
6 | import { useUser } from '../../context/UserContext';
7 | import { RoomPopulated } from '../../types';
8 |
9 | export interface NewRoomProps {
10 | open: boolean;
11 | onClose: (value: null | RoomPopulated) => void;
12 | }
13 |
14 | function NewRoom({ open, onClose }: NewRoomProps) {
15 | const [ isNew, setisNew ] = useState(true);
16 | const [ description, setDescription ] = useState('');
17 | const [ roomCode, setRoomCode ] = useState('');
18 | const chatSocket = useChat();
19 | const { userDetails } = useUser();
20 |
21 | const handleClose = (val: null | RoomPopulated) => {
22 | onClose(val);
23 | };
24 |
25 | const proceed = async (e: React.MouseEvent) => {
26 | e.preventDefault();
27 | if (isNew || (!isNew && roomCode)) {
28 | try {
29 | let { data } = isNew ? await chatHttp.createRoom({ description }) : await chatHttp.joinRoom({ roomCode });
30 | if (data) {
31 | chatSocket.join({ name: userDetails.username, room: data.room.code }, true);
32 | setisNew(true);
33 | setDescription('');
34 | setRoomCode('');
35 | handleClose(data.room);
36 | }
37 | } catch (e) {
38 | console.log(e.response.data);
39 | }
40 | }
41 | };
42 |
43 | return (
44 |
87 | );
88 | }
89 |
90 | export default NewRoom;
91 |
--------------------------------------------------------------------------------
/src/components/NewRoom/style.css:
--------------------------------------------------------------------------------
1 | .newRoom__content {
2 | padding: 1rem;
3 | }
4 | .newRoom__content > form {
5 | text-align: center;
6 | }
7 | .newRoom__content > form input,
8 | .newRoom__content > form textarea {
9 | margin: 0.35rem 0;
10 | width: calc(100% - 2rem);
11 | }
12 | .newRoom__content > form textarea {
13 | resize: none;
14 | }
15 |
16 | .newRoom__type {
17 | margin: 0.35rem auto;
18 | width: 100%;
19 | }
20 | .newRoom__type .newRoom__button {
21 | color: #219ebc;
22 | border: 1px solid rgba(33, 158, 188, 0.5);
23 | }
24 | .newRoom__type .newRoom__button:hover {
25 | border: 1px solid rgba(30, 143, 172, 1);
26 | }
27 | .newRoom__button--selected.MuiButton-outlined {
28 | background-color: #219ebc;
29 | }
30 | .newRoom__button--selected.MuiButton-outlined:hover {
31 | background-color: #1e8fac;
32 | }
33 | .newRoom__button--selected span,
34 | .newRoom__button--selected:hover span {
35 | color: #fff;
36 | }
37 |
38 | .newRoom__content .secondary {
39 | margin: 1.5rem auto 1rem;
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/NotFound/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './style.css';
3 |
4 | function NotFound() {
5 | return (
6 |
9 | );
10 | }
11 |
12 | export default NotFound;
13 |
--------------------------------------------------------------------------------
/src/components/NotFound/style.css:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/rbilag/chat-frontend/06496016f292e3bab4e31b2f1cc1f09dd9fb9c2e/src/components/NotFound/style.css
--------------------------------------------------------------------------------
/src/components/Room/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useEffect, useState, useRef, useCallback } from 'react';
2 | import './style.css';
3 | import Chat from '../Chat';
4 | import Sidebar from '../Sidebar';
5 | import NewRoom from '../NewRoom';
6 | import chatHttp from '../../services/Http';
7 | import RoomDetails from '../RoomDetails';
8 | import { useChat } from '../../context/ChatContext';
9 | import GeneralSnackbar from '../GeneralSnackbar';
10 | import messageAudio from '../../assets/audio/message.mp3';
11 | import { useUser } from '../../context/UserContext';
12 | import { USER_INITIAL_VALUE } from '../../constants';
13 | import { useHistory } from 'react-router-dom';
14 | import { JoinEventResp, LeaveEventResp, RoomPopulated, RoomUserPopulated } from '../../types';
15 |
16 | export interface RoomProps {
17 | history: ReturnType;
18 | }
19 |
20 | const audio = new Audio(messageAudio);
21 |
22 | const Room = ({ history }: RoomProps) => {
23 | const { userDetails, setUserDetails } = useUser();
24 | const [ openModal, setOpenModal ] = useState(false);
25 | const [ openSnackbar, setOpenSnackbar ] = useState(false);
26 | const snackbarMsg = useRef('');
27 | const [ rooms, setRooms ] = useState([] as RoomPopulated[]);
28 | const [ roomCode, setRoomCode ] = useState('');
29 | const chatSocket = useChat();
30 |
31 | const updateUnread = useCallback(
32 | (room: RoomPopulated, willReset: boolean) => {
33 | const userIndex = room.users.findIndex(
34 | (roomUser: RoomUserPopulated) => roomUser.user.username === userDetails.username
35 | );
36 | room.users[userIndex].unread = willReset ? 0 : ++room.users[userIndex].unread;
37 | chatSocket.updateUnread(room.users[userIndex].unread, room.code, userDetails.username);
38 | return room;
39 | },
40 | [ chatSocket, userDetails.username ]
41 | );
42 |
43 | useEffect(
44 | () => {
45 | console.log('Initializing Socket Context..');
46 | chatSocket.init();
47 | chatHttp
48 | .getRooms()
49 | .then(({ data }) => {
50 | setRooms(() => {
51 | if (data.rooms[0]) data.rooms[0] = updateUnread(data.rooms[0], true);
52 | return data.rooms;
53 | });
54 | if (data.rooms[0]) {
55 | setRoomCode(data.rooms[0].code);
56 | data.rooms.forEach((room: RoomPopulated) => {
57 | chatSocket.join({ name: userDetails.username || '', room: room.code });
58 | });
59 | }
60 | })
61 | .catch(({ response }) => {
62 | if (response.status === 401) {
63 | localStorage.clear();
64 | setUserDetails(USER_INITIAL_VALUE);
65 | history.push('/login');
66 | }
67 | });
68 | },
69 | [ history, chatSocket, userDetails.username, setUserDetails, updateUnread ]
70 | );
71 |
72 | useEffect(
73 | () => {
74 | if (chatSocket === null) return;
75 | const joinSubscription = chatSocket.onJoin().subscribe(({ userDetails, joinedRoom }: JoinEventResp) => {
76 | setRooms((prevRooms: RoomPopulated[]) => {
77 | const newRooms = [ ...prevRooms ];
78 | const roomIndex = newRooms.findIndex((room: RoomPopulated) => room.code === joinedRoom);
79 | if (roomIndex >= 0) newRooms[roomIndex].users.push({ user: userDetails, unread: 0 });
80 | return newRooms;
81 | });
82 | });
83 | const leaveSubscription = chatSocket.onLeave().subscribe(({ userDetails, leftRoom }: LeaveEventResp) => {
84 | setRooms((prevRooms: RoomPopulated[]) => {
85 | const newRooms = [ ...prevRooms ];
86 | const roomIndex = newRooms.findIndex((room: RoomPopulated) => room.code === leftRoom);
87 | if (roomIndex >= 0) {
88 | newRooms[roomIndex].users = newRooms[roomIndex].users.filter(
89 | (roomUser: RoomUserPopulated) => roomUser.user.username !== userDetails.username
90 | );
91 | }
92 | return newRooms;
93 | });
94 | });
95 | return () => {
96 | joinSubscription.unsubscribe();
97 | leaveSubscription.unsubscribe();
98 | };
99 | },
100 | [ chatSocket ]
101 | );
102 |
103 | useEffect(
104 | () => {
105 | if (chatSocket === null) return;
106 | const deleteSubscription = chatSocket.onRoomDelete().subscribe((deletedRoom: string) => {
107 | snackbarMsg.current = `Room ${deletedRoom} has been deleted.`;
108 | setOpenSnackbar(true);
109 | if (roomCode === deletedRoom) setRoomCode((roomCode) => '');
110 | setRooms((prevRooms: RoomPopulated[]) => prevRooms.filter((room: RoomPopulated) => room.code !== deletedRoom));
111 | });
112 | const messageSubscription = chatSocket.onMessage().subscribe(({ newMsg, updatedRoom }) => {
113 | setRooms((prevRooms: RoomPopulated[]) => {
114 | const newRooms = [ ...prevRooms ];
115 | const roomIndex = newRooms.findIndex((room: RoomPopulated) => room.code === newMsg.roomCode);
116 | if (roomIndex >= 0) {
117 | if (updatedRoom) newRooms[roomIndex] = updatedRoom;
118 | if (newMsg.roomCode !== roomCode) {
119 | newRooms[roomIndex] = updateUnread(newRooms[roomIndex], false);
120 | audio.play();
121 | }
122 | }
123 | return newRooms;
124 | });
125 | });
126 | return () => {
127 | deleteSubscription.unsubscribe();
128 | messageSubscription.unsubscribe();
129 | };
130 | },
131 | [ chatSocket, roomCode, userDetails.username, updateUnread ]
132 | );
133 |
134 | const getCurrentRoom = () => {
135 | return rooms.find((room: RoomPopulated) => room.code === roomCode);
136 | };
137 |
138 | const handleRoomClick = (code: string) => {
139 | setRoomCode(code);
140 | setRooms((prevRooms: RoomPopulated[]) => {
141 | const newRooms = [ ...prevRooms ];
142 | const roomIndex = newRooms.findIndex((room: RoomPopulated) => room.code === code);
143 | newRooms[roomIndex] = updateUnread(newRooms[roomIndex], true);
144 | return newRooms;
145 | });
146 | };
147 |
148 | const handleRoomLeave = (code: string) => {
149 | setRoomCode('');
150 | setRooms(rooms.filter((room: RoomPopulated) => room.code !== code));
151 | };
152 |
153 | const handleModalClose = (room: RoomPopulated | null) => {
154 | if (room) {
155 | setRooms([ ...rooms, room ]);
156 | setRoomCode(room.code);
157 | }
158 | setOpenModal(false);
159 | };
160 |
161 | return (
162 |
163 |
setOpenModal(true)} rooms={rooms} history={history} onRoomClick={handleRoomClick} />
164 | {roomCode ? (
165 |
166 |
167 |
168 |
169 | ) : (
170 |
171 |
172 |
173 |
174 | {rooms.length > 0 ? 'Click a room to start chatting!' : 'Create or Join a room to start a conversation!'}
175 |
176 |
177 |
178 | )}
179 |
180 |
181 | setOpenSnackbar(false)} />
182 |
183 | );
184 | };
185 |
186 | export default Room;
187 |
--------------------------------------------------------------------------------
/src/components/Room/style.css:
--------------------------------------------------------------------------------
1 | .room {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | background-color: #ededed;
5 | }
6 | .chat--no-room {
7 | flex-grow: 10;
8 | }
9 | .chat--no-room .chat__header {
10 | height: 3rem;
11 | }
12 |
13 | .chat--no-room .chat__body {
14 | height: calc(100vh - 4rem);
15 | display: flex;
16 | justify-content: center;
17 | align-items: center;
18 | }
19 | .chat--no-room .chat__body p {
20 | text-align: center;
21 | font-style: italic;
22 | color: gray;
23 | font-size: 1.5rem;
24 | font-weight: 600;
25 | }
26 |
--------------------------------------------------------------------------------
/src/components/RoomDetails/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState } from 'react';
2 | import './style.css';
3 | import { Avatar, List, ListItem, ListItemAvatar, ListItemIcon, ListItemText } from '@material-ui/core';
4 | import ConfirmationDialog from '../ConfirmationDialog';
5 | import chatHttp from '../../services/Http';
6 | import { useUser } from '../../context/UserContext';
7 | import PhotoIcon from '@material-ui/icons/Photo';
8 | import MeetingRoomIcon from '@material-ui/icons/MeetingRoom';
9 | import DeleteIcon from '@material-ui/icons/Delete';
10 | import PersonIcon from '@material-ui/icons/Person';
11 | import GroupIcon from '@material-ui/icons/Group';
12 | import { RoomPopulated, RoomUserPopulated } from '../../types';
13 |
14 | export interface RoomDetailsProps {
15 | roomDetails: RoomPopulated;
16 | onRoomLeave: (code: string) => void;
17 | }
18 |
19 | function RoomDetails({ roomDetails, onRoomLeave }: RoomDetailsProps) {
20 | const { code, description, users } = roomDetails;
21 | const [ isOpen, setIsOpen ] = useState(false);
22 | const [ content, setContent ] = useState('');
23 | const [ type, setType ] = useState('Leave');
24 | const { userDetails } = useUser();
25 |
26 | const openDialog = (type: string) => {
27 | setIsOpen(true);
28 | setType(type);
29 | if (type === 'Leave') {
30 | setContent(
31 | 'You will not be able to receive messeges sent in this room anymore. Other users in the room will also be notified when you leave.'
32 | );
33 | } else {
34 | setContent('You will not be able to revert this deletion.');
35 | }
36 | };
37 |
38 | const handleModalClose = async (willProceed: boolean) => {
39 | try {
40 | setIsOpen(false);
41 | if (willProceed) {
42 | if (type === 'Leave') {
43 | await chatHttp.leaveRoom({ roomCode: code });
44 | onRoomLeave(code);
45 | } else {
46 | await chatHttp.deleteRoom({ roomCode: code });
47 | }
48 | }
49 | } catch (e) {
50 | console.log(e.response.data);
51 | }
52 | };
53 |
54 | const generateOptions = () => {
55 | const ROOM_OPTIONS = [
56 | { label: 'Change Group Photo', icon: , adminOnly: false },
57 | { label: 'Leave Group', icon: , adminOnly: false, action: () => openDialog('Leave') },
58 | { label: 'Delete Group', icon: , adminOnly: true, action: () => openDialog('Delete') }
59 | ];
60 | return ROOM_OPTIONS.map(({ label, icon, adminOnly, action }, i) => {
61 | return (
62 | (!adminOnly || (adminOnly && users[0].user.username === userDetails.username)) && (
63 |
64 | {icon}
65 |
66 |
67 | )
68 | );
69 | });
70 | };
71 |
72 | const generateUserList = () => {
73 | return users.map(({ user }: RoomUserPopulated) => {
74 | const { username, firstName, lastName } = user;
75 | return (
76 |
77 |
78 | {firstName && lastName ? firstName.charAt(0) + lastName.charAt(0) : }
79 |
80 |
81 |
82 | );
83 | });
84 | };
85 |
86 | return (
87 |
88 |
89 |
90 |
91 |
{code}
92 |
{description}
93 |
{generateOptions()}
94 |
{generateUserList()}
95 |
96 |
97 |
98 | );
99 | }
100 |
101 | export default RoomDetails;
102 |
--------------------------------------------------------------------------------
/src/components/RoomDetails/style.css:
--------------------------------------------------------------------------------
1 | .room__details {
2 | flex-grow: 1;
3 | text-align: center;
4 | padding: 5rem 0;
5 | background-color: #fff;
6 | border: 1px solid #cecece;
7 | }
8 | .room__details .avatar--large {
9 | margin: auto;
10 | }
11 | .room__details h1 {
12 | margin: 1rem 0 0;
13 | font-weight: 700;
14 | }
15 | .room__details p {
16 | margin: 0.25rem 0 1rem;
17 | }
18 |
--------------------------------------------------------------------------------
/src/components/Sidebar/index.tsx:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import './style.css';
3 | import SidebarRoom from '../SidebarRooms';
4 | import { useHistory } from 'react-router-dom';
5 | import Scrollbar from 'react-scrollbars-custom';
6 | import { RoomPopulated } from '../../types';
7 | import SidebarHeader from '../SidebarHeader';
8 | import { useUser } from '../../context/UserContext';
9 |
10 | export interface SidebarProps {
11 | history: ReturnType;
12 | rooms: RoomPopulated[];
13 | onNewRoom: () => void;
14 | onRoomClick: (code: string) => void;
15 | }
16 |
17 | const Sidebar = ({ onNewRoom, rooms, history, onRoomClick }: SidebarProps) => {
18 | const { userDetails } = useUser();
19 | return (
20 |
21 |
22 |
23 | {/* TODO future implementation */}
24 | {/*
*/}
30 |
31 |
32 |
33 | {rooms.map((room: RoomPopulated, i: number) => (
34 |
35 | ))}
36 |
37 |
38 |
39 | );
40 | };
41 |
42 | export default Sidebar;
43 |
--------------------------------------------------------------------------------
/src/components/Sidebar/style.css:
--------------------------------------------------------------------------------
1 | .sidebar {
2 | flex-grow: 1;
3 | background-color: #023047;
4 | color: #fff;
5 | }
6 |
7 | /* TODO revert if has search func */
8 | /* .sidebar__search {
9 | display: flex;
10 | flex-flow: row nowrap;
11 | align-items: center;
12 | background-color: #f6f6f6;
13 | height: 2.3rem;
14 | padding: 0.8rem;
15 | }
16 | .sidebar__searchContainer {
17 | display: flex;
18 | align-items: center;
19 | background-color: white;
20 | width: 100%;
21 | height: 2rem;
22 | border-radius: 1.3rem;
23 | }
24 | .sidebar__searchContainer > .MuiSvgIcon-root {
25 | color: gray;
26 | padding: 0.8rem;
27 | }
28 | .sidebar__searchContainer > input {
29 | border: none;
30 | outline-width: 0;
31 | margin-left: 0.8rem;
32 | } */
33 |
34 | .sidebar__scrollbar {
35 | height: 100%;
36 | width: 100%;
37 | }
38 | .sidebar__rooms {
39 | height: calc(100vh - 3.67rem);
40 | }
41 |
--------------------------------------------------------------------------------
/src/components/SidebarHeader/index.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar, IconButton, Menu, MenuItem } from '@material-ui/core';
2 | import React from 'react';
3 | import ChatIcon from '@material-ui/icons/Chat';
4 | import MoreVertIcon from '@material-ui/icons/MoreVert';
5 | import PersonIcon from '@material-ui/icons/Person';
6 | import { USER_INITIAL_VALUE } from '../../constants';
7 | import { useChat } from '../../context/ChatContext';
8 | import { useUser } from '../../context/UserContext';
9 | import chatHttp from '../../services/Http';
10 | import './style.css';
11 | import { useHistory } from 'react-router-dom';
12 |
13 | export interface SidebarHeaderProps {
14 | history: ReturnType;
15 | onNewRoom: () => void;
16 | }
17 |
18 | function SidebarHeader({ history, onNewRoom }: SidebarHeaderProps) {
19 | const [ anchorEl, setAnchorEl ] = React.useState(null);
20 | const { userDetails, setUserDetails } = useUser();
21 | const chatSocket = useChat();
22 |
23 | const onLogout = () => {
24 | setAnchorEl(null);
25 | console.log('Disconnecting Socket Context..');
26 | chatSocket.disconnect();
27 | chatHttp
28 | .changeLoginStatus({ newValue: false })
29 | .then((resp) => {
30 | localStorage.clear();
31 | setUserDetails(USER_INITIAL_VALUE);
32 | history.push('/login');
33 | })
34 | .catch(({ response }) => {
35 | console.log(response);
36 | });
37 | };
38 | return (
39 |
40 |
41 |
42 | {userDetails.firstName && userDetails.lastName ? (
43 | userDetails.firstName.charAt(0) + userDetails.lastName.charAt(0)
44 | ) : (
45 |
46 | )}
47 |
48 |
{userDetails.firstName + ' ' + userDetails.lastName}
49 |
50 |
51 |
52 |
53 |
54 | setAnchorEl(e.currentTarget)}>
55 |
56 |
57 |
66 |
67 |
68 | );
69 | }
70 |
71 | export default SidebarHeader;
72 |
--------------------------------------------------------------------------------
/src/components/SidebarHeader/style.css:
--------------------------------------------------------------------------------
1 | .sidebar__header {
2 | padding: 0.35rem;
3 | display: flex;
4 | justify-content: space-between;
5 | }
6 | .sidebar__headerAvatar {
7 | display: flex;
8 | }
9 | .sidebar__headerAvatar > .MuiAvatar-root {
10 | margin: auto;
11 | }
12 | .sidebar__headerAvatar p {
13 | margin: auto 0.5rem;
14 | font-weight: 400;
15 | }
16 | .sidebar__headerIcons {
17 | display: flex;
18 | justify-content: flex-end;
19 | }
20 | .sidebar__headerIcons > .MuiIconButton-root {
21 | color: #fff;
22 | margin: 0.125rem 0.25rem 0.125rem 0;
23 | }
24 |
--------------------------------------------------------------------------------
/src/components/SidebarRooms/index.tsx:
--------------------------------------------------------------------------------
1 | import { Avatar } from '@material-ui/core';
2 | import React from 'react';
3 | import GroupIcon from '@material-ui/icons/Group';
4 | import { RoomPopulated, RoomUserPopulated, User } from '../../types';
5 | import './style.css';
6 |
7 | export interface SidebarRoomProps {
8 | room: RoomPopulated;
9 | userDetails: User;
10 | onRoomClick: (code: string) => void;
11 | }
12 |
13 | const SidebarRoom = ({ room, userDetails, onRoomClick }: SidebarRoomProps) => {
14 | const userIndex = room.users.findIndex(
15 | (roomUser: RoomUserPopulated) => roomUser.user.username === userDetails.username
16 | );
17 | return (
18 | onRoomClick(room.code)}>
19 |
20 |
21 |
22 | {userIndex >= 0 && (
23 |
24 | 0 && 'sidebarRoom__details--unread'} `}
26 | >
27 |
{room.code}
28 |
{room.lastMessagePreview || room.description}
29 |
30 | {room.users[userIndex].unread > 0 && (
31 |
32 |
{room.users[userIndex].unread}
33 |
34 | )}
35 |
36 | )}
37 |
38 | );
39 | };
40 |
41 | export default SidebarRoom;
42 |
--------------------------------------------------------------------------------
/src/components/SidebarRooms/style.css:
--------------------------------------------------------------------------------
1 | .sidebarRoom {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | align-items: center;
5 | padding: 0.75rem 1rem;
6 | cursor: pointer;
7 | -webkit-transition: .25s ease-out;
8 | -moz-transition: .25s ease-out;
9 | -ms-transition: .25s ease-out;
10 | -o-transition: .25s ease-out;
11 | transition: .25s ease-out;
12 | }
13 | .sidebarRoom:hover {
14 | background-color: #219ebc;
15 | }
16 | .sidebarRoom__details {
17 | flex-grow: 2;
18 | margin-left: 0.75rem;
19 | }
20 | .sidebarRoom__details > h2,
21 | .sidebarRoom__details > p {
22 | margin: 0;
23 | }
24 | .sidebarRoom__details > h2 {
25 | font-size: 1.2rem;
26 | font-weight: 400;
27 | }
28 | .sidebarRoom__details > p {
29 | color: #cecece;
30 | text-overflow: ellipsis;
31 | white-space: nowrap;
32 | overflow: hidden;
33 | max-width: 13.75rem;
34 | font-size: 0.9rem;
35 | margin: 0.25rem 0;
36 | }
37 | .sidebarRoom__unread p {
38 | padding: 0.25rem;
39 | width: 1rem;
40 | height: 1rem;
41 | font-size: 0.75rem;
42 | background-color: #fb8500;
43 | border-radius: 50%;
44 | color: #fff;
45 | text-align: center;
46 | margin: auto;
47 | }
48 | .sidebarRoom__details--unread > h2,
49 | .sidebarRoom__details--unread > p {
50 | font-weight: 600;
51 | }
52 |
--------------------------------------------------------------------------------
/src/components/Signup/index.tsx:
--------------------------------------------------------------------------------
1 | import React, { useState, useRef } from 'react';
2 | import { DebounceInput } from 'react-debounce-input';
3 | import { Button } from '@material-ui/core';
4 | import chatHttp from '../../services/Http';
5 | import './style.css';
6 | import { useHistory } from 'react-router-dom';
7 |
8 | export interface SignUpProps {
9 | history: ReturnType;
10 | }
11 |
12 | function SignUp({ history }: SignUpProps) {
13 | const [ username, setUsername ] = useState('');
14 | const [ email, setEmail ] = useState('');
15 | const [ isAvailable, setIsAvailable ] = useState({ email: true, username: true });
16 | const firstNameRef = useRef(null);
17 | const lastNameRef = useRef(null);
18 | const passwordRef = useRef(null);
19 | const [ errorMsg, setErrorMsg ] = useState('');
20 |
21 | const proceed = async (e: React.MouseEvent) => {
22 | e.preventDefault();
23 | if (canProceed() && firstNameRef.current && passwordRef.current) {
24 | setErrorMsg('');
25 | chatHttp
26 | .register({
27 | username,
28 | firstName: firstNameRef.current.value,
29 | lastName: lastNameRef.current ? lastNameRef.current.value : '',
30 | email,
31 | password: passwordRef.current.value
32 | })
33 | .then(({ success }) => {
34 | if (success) history.push('/login');
35 | })
36 | .catch(({ response }) => {
37 | console.log(response.data);
38 | });
39 | } else {
40 | setErrorMsg('Fill-in all required fields');
41 | }
42 | };
43 |
44 | const checkAvailability = async (value: string, type: string) => {
45 | if (type === 'email') setEmail(value);
46 | else setUsername(value);
47 | chatHttp
48 | .checkAvailability({ value, type })
49 | .then((resp) => {
50 | if (type === 'email') setIsAvailable({ ...isAvailable, email: resp.isAvailable });
51 | else setIsAvailable({ ...isAvailable, username: resp.isAvailable });
52 | })
53 | .catch(({ response }) => {
54 | console.log(response.data);
55 | });
56 | };
57 |
58 | const canProceed = () => {
59 | return (
60 | username &&
61 | email &&
62 | isAvailable.email &&
63 | isAvailable.username &&
64 | firstNameRef.current &&
65 | passwordRef.current &&
66 | passwordRef.current.value &&
67 | firstNameRef.current.value
68 | );
69 | };
70 |
71 | return (
72 |
119 | );
120 | }
121 |
122 | export default SignUp;
123 |
--------------------------------------------------------------------------------
/src/components/Signup/style.css:
--------------------------------------------------------------------------------
1 | .signup__name {
2 | display: flex;
3 | flex-flow: row nowrap;
4 | width: 100%;
5 | }
6 | .signup__area > form input {
7 | margin: 0.35rem 0.25rem;
8 | width: 100%;
9 | }
10 |
--------------------------------------------------------------------------------
/src/constants.ts:
--------------------------------------------------------------------------------
1 | export enum ChatEvent {
2 | CONNECT = 'connection',
3 | DISCONNECT = 'disconnect',
4 | JOIN = 'join',
5 | MESSAGE = 'message',
6 | UNREAD = 'unread',
7 | LEAVE = 'leave',
8 | ROOM_DELETE = 'room delete'
9 | }
10 | export enum MessageStatus {
11 | SENT = 'sent',
12 | DELIVERED = 'delivered',
13 | SEEN = 'seen',
14 | UNSENT = 'unsent',
15 | ERROR = 'error'
16 | }
17 |
18 | export const USER_INITIAL_VALUE = {
19 | username: '',
20 | firstName: '',
21 | lastName: '',
22 | email: ''
23 | };
24 |
--------------------------------------------------------------------------------
/src/context/ChatContext.ts:
--------------------------------------------------------------------------------
1 | import { createContext, useContext, Context } from 'react';
2 | import { SocketService } from '../services/SocketService';
3 |
4 | export const ChatContext: Context = createContext(new SocketService());
5 |
6 | export const useChat = () => useContext(ChatContext);
7 |
--------------------------------------------------------------------------------
/src/context/UserContext.tsx:
--------------------------------------------------------------------------------
1 | import { createContext, useContext } from 'react';
2 | import { USER_INITIAL_VALUE } from '../constants';
3 | import { User } from '../types';
4 |
5 | export type UserContextType = {
6 | userDetails: User;
7 | setUserDetails: (userDetails: User) => void;
8 | };
9 |
10 | export const UserContext = createContext({
11 | userDetails: USER_INITIAL_VALUE,
12 | setUserDetails: () => {}
13 | });
14 | export const useUser = () => useContext(UserContext);
15 |
--------------------------------------------------------------------------------
/src/index.css:
--------------------------------------------------------------------------------
1 | body {
2 | margin: 0;
3 | -webkit-font-smoothing: antialiased;
4 | -moz-osx-font-smoothing: grayscale;
5 | }
6 | h1,
7 | h2,
8 | h3,
9 | h4,
10 | h5,
11 | h6,
12 | .header__text,
13 | .MuiButton-root,
14 | .MuiTypography-h6 {
15 | font-family: 'Exo 2', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell',
16 | 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
17 | }
18 |
19 | body,
20 | input,
21 | textarea,
22 | .MuiTypography-body1,
23 | .MuiTypography-body2,
24 | .MuiSnackbarContent-root,
25 | .MuiMenuItem-root {
26 | font-family: 'Source Sans Pro', -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu',
27 | 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif;
28 | }
29 |
30 | code {
31 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', monospace;
32 | }
33 |
34 | h1,
35 | h2,
36 | h3,
37 | h4,
38 | h5,
39 | h6,
40 | .header__text {
41 | font-weight: 800;
42 | }
43 |
--------------------------------------------------------------------------------
/src/index.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import ReactDOM from 'react-dom';
3 | import App from './components/App';
4 | import './index.css';
5 | import reportWebVitals from './reportWebVitals';
6 |
7 | ReactDOM.render(, document.getElementById('root'));
8 |
9 | // If you want to start measuring performance in your app, pass a function
10 | // to log results (for example: reportWebVitals(console.log))
11 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
12 | reportWebVitals();
13 |
--------------------------------------------------------------------------------
/src/react-app-env.d.ts:
--------------------------------------------------------------------------------
1 | ///
2 | declare module '*.mp3' {
3 | const src: string;
4 | export default src;
5 | }
6 |
--------------------------------------------------------------------------------
/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/services/Axios.ts:
--------------------------------------------------------------------------------
1 | import axios from 'axios';
2 |
3 | const instance = axios.create({
4 | baseURL: `${process.env.REACT_APP_SERVER_URL}/api/v1`
5 | });
6 |
7 | instance.interceptors.request.use((req) => {
8 | console.log(`${req.method} ${req.url}`);
9 | if (req.url !== '/register' && req.url !== '/login') {
10 | req.headers = { ...req.headers, Authorization: `Basic ${localStorage.getItem('chat-app-auth')}` };
11 | }
12 | return req;
13 | });
14 |
15 | export default instance;
16 |
--------------------------------------------------------------------------------
/src/services/Http.ts:
--------------------------------------------------------------------------------
1 | import {
2 | CredAvailabilityData,
3 | RegisterData,
4 | LoginData,
5 | LoginStatusData,
6 | NewRoomData,
7 | RoomData,
8 | CredAvailabilityResp,
9 | BaseResponse,
10 | LoginResp,
11 | UserResp,
12 | RoomResp,
13 | RoomsResp,
14 | MessagesResp
15 | } from './../types';
16 | import axios from './Axios';
17 |
18 | const checkAvailability = async (data: CredAvailabilityData) => {
19 | return new Promise(async (resolve, reject) => {
20 | try {
21 | const response = await axios.post('/users/checkAvailability', data);
22 | resolve(response.data);
23 | } catch (error) {
24 | reject(error);
25 | }
26 | });
27 | };
28 |
29 | const register = async (data: RegisterData) => {
30 | return new Promise(async (resolve, reject) => {
31 | try {
32 | const response = await axios.post('/register', data);
33 | resolve(response.data);
34 | } catch (error) {
35 | reject(error);
36 | }
37 | });
38 | };
39 |
40 | const login = async (data: LoginData) => {
41 | return new Promise(async (resolve, reject) => {
42 | try {
43 | const response = await axios.post('/login', data);
44 | resolve(response.data);
45 | } catch (error) {
46 | reject(error);
47 | }
48 | });
49 | };
50 |
51 | const changeLoginStatus = async (data: LoginStatusData) => {
52 | return new Promise(async (resolve, reject) => {
53 | try {
54 | const response = await axios.post('/users/changeStatus', data);
55 | resolve(response.data);
56 | } catch (error) {
57 | reject(error);
58 | }
59 | });
60 | };
61 |
62 | const createRoom = async (data: NewRoomData) => {
63 | return new Promise(async (resolve, reject) => {
64 | try {
65 | const response = await axios.post('/rooms/new', data);
66 | resolve(response.data);
67 | } catch (error) {
68 | reject(error);
69 | }
70 | });
71 | };
72 |
73 | const joinRoom = async (data: RoomData) => {
74 | return new Promise(async (resolve, reject) => {
75 | try {
76 | const response = await axios.post('/rooms/join', data);
77 | resolve(response.data);
78 | } catch (error) {
79 | reject(error);
80 | }
81 | });
82 | };
83 |
84 | const leaveRoom = async (data: RoomData) => {
85 | return new Promise(async (resolve, reject) => {
86 | try {
87 | const response = await axios.post('/rooms/leave', data);
88 | resolve(response.data);
89 | } catch (error) {
90 | reject(error);
91 | }
92 | });
93 | };
94 |
95 | const getRooms = async () => {
96 | return new Promise(async (resolve, reject) => {
97 | try {
98 | const response = await axios.get('/rooms');
99 | resolve(response.data);
100 | } catch (error) {
101 | reject(error);
102 | }
103 | });
104 | };
105 |
106 | const deleteRoom = async (data: RoomData) => {
107 | return new Promise(async (resolve, reject) => {
108 | try {
109 | const response = await axios.post('/rooms/delete', data);
110 | resolve(response.data);
111 | } catch (error) {
112 | reject(error);
113 | }
114 | });
115 | };
116 |
117 | const getMessages = async (data: RoomData) => {
118 | return new Promise(async (resolve, reject) => {
119 | try {
120 | const response = await axios.post('/messages', data);
121 | resolve(response.data);
122 | } catch (error) {
123 | reject(error);
124 | }
125 | });
126 | };
127 |
128 | const chatHttp = {
129 | checkAvailability,
130 | changeLoginStatus,
131 | register,
132 | login,
133 | createRoom,
134 | joinRoom,
135 | leaveRoom,
136 | getRooms,
137 | getMessages,
138 | deleteRoom
139 | };
140 |
141 | export default chatHttp;
142 |
--------------------------------------------------------------------------------
/src/services/SocketService.ts:
--------------------------------------------------------------------------------
1 | import { UserRoom, LeaveEventResp, JoinEventResp, ChatMessage, MessageEventResp } from './../types';
2 | import { ChatEvent } from '../constants';
3 | import io from 'socket.io-client';
4 | import { fromEvent, Observable } from 'rxjs';
5 |
6 | export class SocketService {
7 | private socket: SocketIOClient.Socket = {} as SocketIOClient.Socket;
8 |
9 | public init(): SocketService {
10 | console.log('Initializing Socket Service');
11 | this.socket = io(process.env.REACT_APP_SERVER_URL!);
12 | return this;
13 | }
14 |
15 | public join(userRoom: UserRoom, isFirst: boolean = false): void {
16 | console.log(`${userRoom.name} joined ${userRoom.room}`);
17 | this.socket.emit(ChatEvent.JOIN, { userRoom, isFirst });
18 | }
19 |
20 | public send(message: ChatMessage): void {
21 | console.log('Sending Message: ' + message);
22 | this.socket.emit(ChatEvent.MESSAGE, message);
23 | }
24 |
25 | public updateUnread(unread: number, roomCode: string, username: string): void {
26 | this.socket.emit(ChatEvent.UNREAD, { unread, roomCode, username });
27 | }
28 |
29 | public onJoin(): Observable {
30 | return fromEvent(this.socket, ChatEvent.JOIN);
31 | }
32 |
33 | public onLeave(): Observable {
34 | return fromEvent(this.socket, ChatEvent.LEAVE);
35 | }
36 |
37 | public onRoomDelete(): Observable {
38 | return fromEvent(this.socket, ChatEvent.ROOM_DELETE);
39 | }
40 |
41 | public onMessage(): Observable {
42 | return fromEvent(this.socket, ChatEvent.MESSAGE);
43 | }
44 |
45 | public disconnect(): void {
46 | console.log('Disconnecting...');
47 | this.socket.disconnect();
48 | }
49 | }
50 |
--------------------------------------------------------------------------------
/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 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface User {
2 | username: string;
3 | firstName?: string;
4 | lastName?: string;
5 | email?: string;
6 | }
7 |
8 | export interface MessagePopulated {
9 | content: string;
10 | status?: string;
11 | isSystem?: boolean;
12 | user: User;
13 | roomCode: string;
14 | createdAt: string;
15 | }
16 |
17 | export interface RoomUserPopulated {
18 | user: User;
19 | unread: number;
20 | }
21 |
22 | export interface RoomPopulated {
23 | code: string;
24 | description: string;
25 | lastActivity?: Date;
26 | lastMessagePreview?: string;
27 | users: Array;
28 | createdAt: string;
29 | }
30 |
31 | export interface UserRoom {
32 | name: string;
33 | room: string;
34 | }
35 |
36 | export interface ChatMessage {
37 | userRoom: UserRoom;
38 | content: string;
39 | status?: string;
40 | isSystem?: boolean;
41 | }
42 |
43 | export interface CredAvailabilityData {
44 | value: string;
45 | type: string;
46 | }
47 | export interface LoginData {
48 | username: string;
49 | password: string;
50 | }
51 | export interface RegisterData extends LoginData {
52 | firstName: string;
53 | lastName?: string;
54 | email: string;
55 | }
56 | export interface LoginStatusData {
57 | newValue: boolean;
58 | }
59 | export interface NewRoomData {
60 | description: string;
61 | }
62 | export interface RoomData {
63 | roomCode: string;
64 | }
65 |
66 | export interface BaseResponse {
67 | success: boolean;
68 | }
69 | export interface CredAvailabilityResp extends BaseResponse {
70 | isAvailable: boolean;
71 | }
72 | export interface LoginResp extends BaseResponse {
73 | authorization: string;
74 | data: {
75 | userDetails: User;
76 | };
77 | }
78 | export interface UserResp extends BaseResponse {
79 | user: User;
80 | }
81 | export interface RoomResp extends BaseResponse {
82 | data: {
83 | room: RoomPopulated;
84 | };
85 | }
86 | export interface RoomsResp extends BaseResponse {
87 | data: {
88 | rooms: RoomPopulated[];
89 | };
90 | }
91 | export interface RoomEventResp {
92 | userDetails: User;
93 | }
94 | export interface JoinEventResp extends RoomEventResp {
95 | joinedRoom: string;
96 | }
97 | export interface LeaveEventResp extends RoomEventResp {
98 | leftRoom: string;
99 | }
100 | export interface MessageEventResp {
101 | newMsg: MessagePopulated;
102 | updatedRoom: RoomPopulated;
103 | }
104 | export interface MessagesResp extends BaseResponse {
105 | data: {
106 | messages: MessagePopulated[];
107 | };
108 | }
109 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "target": "es5",
4 | "module": "esnext",
5 | "lib": [
6 | "dom",
7 | "dom.iterable",
8 | "esnext"
9 | ],
10 | "allowJs": true,
11 | "jsx": "react-jsx",
12 | "noEmit": true,
13 | "isolatedModules": true,
14 | "strict": true,
15 | "moduleResolution": "node",
16 | "allowSyntheticDefaultImports": true,
17 | "esModuleInterop": true,
18 | "skipLibCheck": true,
19 | "forceConsistentCasingInFileNames": true,
20 | "noFallthroughCasesInSwitch": true,
21 | "resolveJsonModule": true
22 | },
23 | "include": [
24 | "src"
25 | ]
26 | }
27 |
--------------------------------------------------------------------------------