├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .gitignore ├── .prettierrc ├── .travis.yml ├── CNAME ├── CONTRIBUTING.md ├── Procfile ├── README.md ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html └── manifest.json └── src ├── app ├── App.css ├── App.js ├── App.test.js └── assets │ ├── image │ ├── chatIcon.png │ └── loops.png │ ├── manifest.json │ └── sound │ └── ping.mp3 ├── chatPage ├── chatIconPushNotification.png ├── chatPage.css ├── chatPage.js ├── test.js └── user.png ├── index.js ├── loginPage ├── loginPage.css └── loginPage.js └── registerServiceWorker.js /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | end_of_line = lf 7 | indent_size = 2 8 | indent_style = space 9 | insert_final_newline = true 10 | max_line_length = 80 11 | trim_trailing_whitespace = true 12 | 13 | [*.md] 14 | max_line_length = 0 15 | trim_trailing_whitespace = false 16 | 17 | [COMMIT_EDITMSG] 18 | max_line_length = 0 19 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OTRChat/NodeChat/7a574466c36e89f31e570bba8399be20fd44ef28/.eslintignore -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parser": "babel-eslint", 3 | "extends": [ 4 | "airbnb", 5 | "prettier", 6 | "prettier/react" 7 | ], 8 | "plugins": [ 9 | "prettier" 10 | ], 11 | "rules": { 12 | "prettier/prettier": ["error"], 13 | "quotes": ["error", "double"], 14 | "indent": ["error", "tab"], 15 | "react/jsx-indent": ["error", "tab"], 16 | "react/jsx-one-expression-per-line": 0, 17 | "react/jsx-filename-extension": [1, {"extensions": [".js",".jsx"]}], 18 | "react/prop-types": 0, 19 | "no-underscore-dangle": 0, 20 | "import/imports-first": ["error","absolute-first"], 21 | "import/newline-after-import": "error", 22 | "no-undef": [1], 23 | "no-console": [1] 24 | } 25 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=false 2 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | 6 | # testing 7 | /coverage 8 | 9 | # production 10 | /build 11 | 12 | # misc 13 | .DS_Store 14 | .env.local 15 | .env.development.local 16 | .env.test.local 17 | .env.production.local 18 | 19 | npm-debug.log* 20 | yarn-debug.log* 21 | yarn-error.log* 22 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "trailingComma": "es5", 3 | "useTabs": true, 4 | bracketSpacing: true, 5 | jsxBracketSameLine: true, 6 | semi: true 7 | } -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "stable" 4 | 5 | script: 6 | - npm run-script lint 7 | -------------------------------------------------------------------------------- /CNAME: -------------------------------------------------------------------------------- 1 | chat.joshghent.com 2 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing 2 | 3 | ### Thank you for your interest in contributing to this project! 4 | 5 | Following these guidelines helps to communicate that you respect the time of the developers managing and developing this open source project. In return, they should reciprocate that respect in addressing your issue, assessing changes, and helping you finalize your pull requests. 6 | 7 | ### Contributions 8 | There are many ways to contribute, from improving the documentation, bug reports, feature requests or taking on a existing issues. 9 | 10 | # Code of conduct 11 | In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone 12 | 13 | When working on a issue, please post in that issue that you would like to work on it. A moderator will assign it to you and change the label. This will help clarify to others that a issue is already being worked on. 14 | 15 | # Getting started 16 | 17 | To start the app you need to have both the server and the react app running. 18 | 19 | ### Starting the server 20 | The server is available here https://github.com/OTRChat/server. 21 | Inside the server folder run the following commands. 22 | ```bash 23 | # Install the dependencies 24 | npm install 25 | 26 | # Run the server 27 | npm start 28 | ``` 29 | ### Starting the react app 30 | Inside the react app folder run the following commands. 31 | ```bash 32 | # Install the dependencies 33 | npm install 34 | 35 | # Run the react app 36 | npm start 37 | ``` 38 | 39 | ### Steps to submitting a contribution 40 | 1. Create your own fork of the code 41 | 2. Do the changes in your fork 42 | 3. If you like the change and think the project could use it send a pull request with your changes. 43 | 44 | # How to report a bug 45 | 46 | File an issue, make sure to answer these three questions: 47 | 1. What did you do? 48 | 2. What did you expect to see? 49 | 3. What did you see instead? 50 | 51 | # How to suggest a feature 52 | File an issue, explain the feature you want added and a moderator will review it. 53 | 54 | # Issue labeling conventions 55 | * Availble - an issue that is open for someone to work on. 56 | * Assigned - an issue that a someone is working on. 57 | * Good First Issue - issues that are smaller in size. 58 | * High Prioriy - an issue that we want to get resolved quickly. 59 | * Discussion - an issue that is being used to discuss a possible change or idea. 60 | -------------------------------------------------------------------------------- /Procfile: -------------------------------------------------------------------------------- 1 | web: npm start 2 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # NodeChat :speech_balloon: 2 | 3 | A group chat application written in Node and SocketIO. 4 | 5 | See it live [here](https://chat.joshghent.com). 6 | 7 | ## Project Vision 8 | 9 | To develop a robust chat application, that is able to act as a stand alone application and/or be embeded inside of a existing application. 10 | 11 | ### Use Cases 12 | * StandAlone Chat website 13 | * Customer Support Chat 14 | * Live Open Chat on a website 15 | * Private chat space on your website managed by you instead of thirdParty like Slack or Skype. 16 | 17 | ## Project Goal 18 | * Publish on [NPM](www.npmjs.com). 19 | 20 | ### Quick Start Guide 21 | To start the app you need to have both the server and the react app running. 22 | 23 | ### Starting the server 24 | The server is available here https://github.com/OTRChat/server. 25 | Inside the server folder run the following commands. 26 | ```bash 27 | # Install the dependencies 28 | npm install 29 | 30 | # Run the server 31 | npm start 32 | ``` 33 | ### Starting the react app 34 | Inside the react app folder run the following commands. 35 | ```bash 36 | # Install the dependencies 37 | npm install 38 | 39 | # Run the react app 40 | npm start 41 | ``` 42 | 43 | ### URL's 44 | The app is deployed with Heroku and configured to auto deploy the develop and master branch 45 | Development: https://joshghent-nodechat-staging.herokuapp.com/ 46 | Master: https://chat.joshghent.com/ 47 | 48 | Server development: https://joshghent-nodechat-server.herokuapp.com/ 49 | Server master: https://joshghent-nodechat-server.herokuapp.com/ 50 | 51 | If environment variables need updating as part of an update, then please ask one of the maintainers 52 | 53 | ### Contributing to this Project 54 | 55 | This project is open to anyone. Please read our CONTRIBUTING.md for more information. 56 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-chat", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "Group chat with SocketIO", 6 | "main": "index.js", 7 | "author": "Josh Ghent", 8 | "license": "MIT", 9 | "scripts": { 10 | "lint": "prettier-eslint ./src/**/*.{js,jsx}", 11 | "start": "react-scripts start", 12 | "build": "react-scripts build", 13 | "test": "react-scripts test --env=jsdom", 14 | "eject": "react-scripts eject", 15 | "fix-code": "prettier-eslint --write 'src/**/*.{js,jsx}' ", 16 | "fix-styles": "prettier-stylelint --write 'src/**/*.{css,scss}' " 17 | }, 18 | "repository": { 19 | "type": "git", 20 | "url": "git@github.com:joshghent/NodeChat.git" 21 | }, 22 | "devDependencies": { 23 | "eslint": "^5.9.0", 24 | "eslint-config-airbnb": "^17.1.0", 25 | "eslint-config-prettier": "^3.3.0", 26 | "eslint-plugin-prettier": "^3.0.0", 27 | "eslint-plugin-react": "^7.11.1", 28 | "prettier": "^1.15.3", 29 | "prettier-eslint": "^8.8.2", 30 | "prettier-eslint-cli": "^4.7.1", 31 | "prettier-stylelint": "^0.4.2" 32 | }, 33 | "dependencies": { 34 | "push.js": "1.0.9", 35 | "react": "^16.5.2", 36 | "react-dom": "^16.5.2", 37 | "react-scripts": "2.1.1", 38 | "shortid": "2.2.14", 39 | "socket.io": "^2.2.0", 40 | "socket.io-client": "^2.1.1" 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OTRChat/NodeChat/7a574466c36e89f31e570bba8399be20fd44ef28/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 14 | 23 | React App 24 | 25 | 26 | 27 | 30 |
31 | 41 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /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 | "start_url": "./index.html", 12 | "display": "standalone", 13 | "theme_color": "#000000", 14 | "background_color": "#ffffff" 15 | } 16 | -------------------------------------------------------------------------------- /src/app/App.css: -------------------------------------------------------------------------------- 1 | * { 2 | margin: 0; 3 | padding: 0; 4 | box-sizing: border-box; 5 | font-family: 'Lucida Grande', Tahoma, Verdana, Arial, sans-serif; } 6 | 7 | .message { 8 | clear: both; 9 | min-width: 85px; 10 | margin-bottom: 25px;} 11 | 12 | .inputContainer { 13 | position: fixed; 14 | bottom: 0; 15 | width: 100%; } 16 | 17 | .inputContainer .chatBox { 18 | background: #d2d2d2; 19 | padding: 3px; 20 | } 21 | 22 | #message_text { 23 | border: 0; 24 | padding: 10px; 25 | width: 100%; 26 | margin-right: 0.5%; } 27 | #message_text:focus { 28 | outline: none; } 29 | 30 | #chat_log { 31 | list-style-type: none; 32 | margin: 0; 33 | padding: 0; 34 | overflow-x: hidden; 35 | height: 100%; 36 | padding-bottom: 15px; } 37 | 38 | #usernameInput { 39 | background-color: transparent; 40 | outline: none; 41 | border: none; 42 | border-bottom: 1px solid #fff; 43 | text-align: center; 44 | width: 300px; 45 | color: #fff; 46 | font-size: 20px; 47 | margin-top: 10px; } 48 | 49 | .messageTime { 50 | float: right; 51 | font-size: 11px; 52 | display: table-cell; 53 | bottom: 0; 54 | position: absolute; 55 | right: 0; 56 | margin-right: 20px; 57 | margin-bottom:2px; 58 | opacity: 0.5; } 59 | 60 | .whiteText { 61 | color: white; } 62 | 63 | .blackText { 64 | color: black; } 65 | 66 | .messageBody { 67 | margin-right: 25px; } 68 | 69 | .username { 70 | font-weight: bold; 71 | margin-right: 2px; } 72 | .username:after { 73 | content: ':'; } 74 | 75 | .image_message { 76 | width: 300px; 77 | height: auto; } 78 | 79 | .pages { 80 | height: 100%; 81 | width: 100%; 82 | margin: 0; 83 | padding: 0; 84 | text-decoration: none; } 85 | 86 | .page { 87 | width: 100%; 88 | height: 100%; 89 | position: fixed; } 90 | 91 | .chatPage { 92 | background-image: url("./assets/image/loops.png"); 93 | background-repeat: repeat; 94 | height: 96%; 95 | padding-top: 10px;} 96 | 97 | .from-me { 98 | position: relative; 99 | background: #0089FF; 100 | color: white; 101 | float: right; 102 | padding: 15px 25px; 103 | border-radius: 25px; 104 | margin-right: 40px; 105 | padding-left: 10px; } 106 | .from-me:after { 107 | left: 97%; 108 | top: 15%; 109 | border: solid transparent; 110 | content: " "; 111 | height: 0; 112 | width: 0; 113 | position: absolute; 114 | pointer-events: none; 115 | border-color: rgba(0, 187, 255, 0); 116 | border-left-color: #0089FF; 117 | border-width: 10px; 118 | margin-top: 7px; } 119 | 120 | .from-them { 121 | position: relative; 122 | background: #D7D7D7; 123 | float: left; 124 | padding: 15px 25px; 125 | border-radius: 25px; 126 | margin-left: 40px; } 127 | .from-them:after { 128 | right: 98%; 129 | top: 15%; 130 | border: solid transparent; 131 | content: " "; 132 | height: 0; 133 | width: 0; 134 | position: absolute; 135 | pointer-events: none; 136 | border-right-color: #D7D7D7; 137 | border-width: 10px; 138 | margin-top: 7px; } 139 | -------------------------------------------------------------------------------- /src/app/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./App.css"; 3 | 4 | import LoginPage from "../loginPage/loginPage.js"; 5 | 6 | class App extends Component { 7 | render() { 8 | return ( 9 |
10 | 11 |
12 | ); 13 | } 14 | } 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /src/app/App.test.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App"; 4 | 5 | it("renders without crashing", () => { 6 | const div = document.createElement("div"); 7 | ReactDOM.render(, div); 8 | ReactDOM.unmountComponentAtNode(div); 9 | }); 10 | -------------------------------------------------------------------------------- /src/app/assets/image/chatIcon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OTRChat/NodeChat/7a574466c36e89f31e570bba8399be20fd44ef28/src/app/assets/image/chatIcon.png -------------------------------------------------------------------------------- /src/app/assets/image/loops.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OTRChat/NodeChat/7a574466c36e89f31e570bba8399be20fd44ef28/src/app/assets/image/loops.png -------------------------------------------------------------------------------- /src/app/assets/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "NodeChat", 3 | "name": "NodeChat", 4 | "icons": [], 5 | "start_url": "/index.html", 6 | "display": "standalone" 7 | } -------------------------------------------------------------------------------- /src/app/assets/sound/ping.mp3: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OTRChat/NodeChat/7a574466c36e89f31e570bba8399be20fd44ef28/src/app/assets/sound/ping.mp3 -------------------------------------------------------------------------------- /src/chatPage/chatIconPushNotification.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OTRChat/NodeChat/7a574466c36e89f31e570bba8399be20fd44ef28/src/chatPage/chatIconPushNotification.png -------------------------------------------------------------------------------- /src/chatPage/chatPage.css: -------------------------------------------------------------------------------- 1 | .topnav { 2 | background-color: rgb(208, 207, 207); 3 | overflow: hidden; 4 | } 5 | 6 | .topnav input { 7 | float: left; 8 | color: white; 9 | background-color: #808080; 10 | text-align: center; 11 | padding: 12px 14px; 12 | text-decoration: none; 13 | font-size: 14px; 14 | } 15 | 16 | .topnav input:hover { 17 | background-color: #7b5e5e; 18 | color: black; 19 | } 20 | 21 | .topnav-right { 22 | float: right; 23 | } 24 | 25 | .pageBottomReference { 26 | clear: both; 27 | margin-bottom: 65px;} 28 | 29 | .userName { 30 | color: black; 31 | position: relative; 32 | top: 5px;} 33 | 34 | .from-them-userPic { 35 | width: 300px; 36 | height: 25px; 37 | position: absolute; 38 | left: -35px; 39 | top: 15px;} 40 | 41 | .from-me-userPic { 42 | position: absolute; 43 | right: -40px;} 44 | 45 | .from-me-userPic>img { 46 | right: -8px;} 47 | 48 | .from-them-userPic>img { 49 | right: -4px;} 50 | 51 | .from-them .messageBody{ 52 | display: inline-block; 53 | min-width: 52px; 54 | margin-right: -10px; 55 | overflow-wrap: break-word; 56 | max-width: 300px;} 57 | 58 | .from-me .messageBody{ 59 | display: inline-block; 60 | min-width: 45px; 61 | margin-right: -5px; 62 | text-align: right; 63 | overflow-wrap: break-word; 64 | max-width: 300px;} 65 | 66 | .from-them .messageTime { 67 | left: 25px; 68 | } 69 | 70 | .userNamePic { 71 | width: 25px; 72 | height: 25px; 73 | position: relative; 74 | right: 2px;} 75 | 76 | .isTyping{ 77 | padding: .5%; 78 | } 79 | 80 | .isTyping li{ 81 | display: inline; 82 | } 83 | 84 | .message { 85 | clear: both; 86 | margin-bottom: 25px;} 87 | 88 | .inputContainer { 89 | position: fixed; 90 | bottom: 0; 91 | width: 100%; } 92 | 93 | .inputContainer .chatBox { 94 | background: #d2d2d2; 95 | padding: 3px;} 96 | 97 | #message_text { 98 | border: 0; 99 | padding: 10px; 100 | width: 100%; 101 | margin-right: 0.5%; } 102 | 103 | #message_text:focus { 104 | outline: none; } 105 | 106 | #chat_log { 107 | list-style-type: none; 108 | margin: 0; 109 | padding: 0; 110 | overflow-x: hidden; 111 | height: 93%; 112 | padding-top: 10px; 113 | margin-top: -11px; } 114 | 115 | .messageTime { 116 | float: right; 117 | font-size: 11px; 118 | display: table-cell; 119 | bottom: 0; 120 | position: absolute; 121 | right: 0; 122 | margin-right: 20px; 123 | opacity: 0.5; } 124 | 125 | .chatPage { 126 | background-image: url("../app/assets/image/loops.png"); 127 | background-repeat: repeat; 128 | height: 96%; 129 | padding-top: 10px;} 130 | 131 | .from-me { 132 | position: relative; 133 | background: #0089FF; 134 | color: white; 135 | float: right; 136 | padding: 15px 25px; 137 | border-radius: 25px; 138 | margin-right: 45px; 139 | padding-left: 10px; } 140 | 141 | .from-me:after { 142 | left: 97%; 143 | top: 15%; 144 | border: solid transparent; 145 | content: " "; 146 | height: 0; 147 | width: 0; 148 | position: absolute; 149 | pointer-events: none; 150 | border-color: rgba(0, 187, 255, 0); 151 | border-left-color: #0089FF; 152 | border-width: 10px; 153 | margin-top: 7px; } 154 | 155 | .from-them { 156 | position: relative; 157 | background: #D7D7D7; 158 | float: left; 159 | padding: 15px 25px; 160 | border-radius: 25px; 161 | margin-left: 40px; } 162 | .from-them:after { 163 | right: 98%; 164 | top: 15%; 165 | border: solid transparent; 166 | content: " "; 167 | height: 0; 168 | width: 0; 169 | position: absolute; 170 | pointer-events: none; 171 | border-right-color: #D7D7D7; 172 | border-width: 10px; 173 | margin-top: 7px; } 174 | 175 | .systemLog { 176 | text-align: center; 177 | color: gray; 178 | padding: 0.2em; 179 | width: 100%;} 180 | -------------------------------------------------------------------------------- /src/chatPage/chatPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./chatPage.css"; 3 | import * as Push from "push.js"; 4 | import mp3_file from "../app/assets/sound/ping.mp3"; 5 | import userImg from "./user.png"; 6 | import chatIcon from "./chatIconPushNotification.png"; 7 | import LoginPage from "../loginPage/loginPage.js"; 8 | 9 | class ChatPage extends Component { 10 | constructor(props) { 11 | super(props); 12 | this.state = { 13 | socket: props.socket, 14 | chatInput: "", 15 | chatLog: [], 16 | greeting: "", 17 | connected: props.connected, 18 | username: props.username, 19 | avatar: userImg, 20 | userIsTyping: [], 21 | previousUser: props.previousUser, 22 | logout: false, 23 | }; 24 | this.setChatInput = this.setChatInput.bind(this); 25 | this.enterKeyPress = this.enterKeyPress.bind(this); 26 | this.Logout = this.Logout.bind(this); 27 | this.Avatar = this.Avatar.bind(this); 28 | } 29 | 30 | componentDidMount() { 31 | if (this.state.connected) { 32 | // Create a greeting message for the newly connected user 33 | if (!this.state.previousUser) { 34 | this.greetUser(); 35 | } 36 | // setup default avatar 37 | this.setState({ avatar: userImg }); 38 | this.state.socket.emit("add avatar", this.state.avatar); 39 | 40 | this.state.socket.on("new message", message => { 41 | this.addChatMessage(message); 42 | if (this.state.username !== message.username) { 43 | this.notifyUser(message); 44 | } 45 | }); 46 | this.state.socket.on("typing", user => { 47 | if (this.state.username !== user.username) { 48 | if (!this.state.userIsTyping.find(users => users === user.username)) { 49 | this.setState({ 50 | userIsTyping: [...this.state.userIsTyping, user.username], 51 | }); 52 | } 53 | } 54 | }); 55 | this.state.socket.on("stop typing", user => { 56 | if (this.state.username !== user.username) { 57 | this.setState({ 58 | userIsTyping: this.state.userIsTyping.filter( 59 | users => users !== user.username 60 | ), 61 | }); 62 | } 63 | }); 64 | this.state.socket.on("user join", username => { 65 | this.addSystemMessage(`User ${username} joined the room.`); 66 | }); 67 | this.state.socket.on("user disconnected", username => { 68 | this.addSystemMessage(`User ${username.username} has left the room`); 69 | }); 70 | } 71 | } 72 | 73 | greetUser() { 74 | // Create a span with the greeting message 75 | const userGreeting = ( 76 | Hello there {this.state.username}! 77 | ); 78 | 79 | // Create a container for the message text 80 | const greetingContainer =
  • {userGreeting}
  • ; 81 | 82 | // Add the greeting to the chat log 83 | this.setState({ greeting: greetingContainer }); 84 | } 85 | 86 | enterKeyPress(e) { 87 | if (e.keyCode === 13) { 88 | this.sendMessage(); 89 | this.setState({ chatInput: "" }); 90 | this.state.socket.emit("stop typing", this.state.username); 91 | } 92 | } 93 | 94 | setChatInput(event) { 95 | if (event.target.value !== "") { 96 | this.state.socket.emit("typing", this.state.username); 97 | } else { 98 | this.state.socket.emit("stop typing", this.state.username); 99 | } 100 | this.setState({ chatInput: event.target.value }); 101 | } 102 | 103 | sendMessage() { 104 | // Get the input of the chat message 105 | const messageText = this.state.chatInput; 106 | 107 | // If there is a message and we are connected to the server 108 | if (messageText && this.state.connected) { 109 | // Call our function to add the message to the chat log. 110 | // We do this because we don't need to get our own message back from the user to display it. 111 | this.addChatMessage({ 112 | username: this.state.username, 113 | message: messageText, 114 | messageClass: "from-me whiteText", 115 | }); 116 | 117 | // Tell the server that we have sent a message. 118 | // This will trigger a broadcast to the other users 119 | this.state.socket.emit("new message", messageText); 120 | } 121 | } 122 | 123 | addChatMessage(data) { 124 | const date = new Date().toLocaleTimeString([], { 125 | hour: "2-digit", 126 | minute: "2-digit", 127 | }); 128 | let messageSenderClass = ""; 129 | let UserName = data.username; 130 | const messageBody = this.parseMessageText(data.message); 131 | let avatar = data.avatar; 132 | 133 | if (data.messageClass !== "from-me whiteText") { 134 | messageSenderClass = "from-them-userPic"; 135 | } else { 136 | messageSenderClass = "from-me-userPic"; 137 | UserName = this.state.username; 138 | avatar = this.state.avatar; 139 | } 140 | 141 | const element = ( 142 |
    143 |
    144 | 145 |

    {UserName}

    146 |
    147 |
    148 | {messageBody} 149 | {date} 150 |
    151 |
    152 | ); 153 | 154 | const container = { 155 | class: data.messageClass, 156 | element, 157 | }; 158 | this.setState({ chatLog: [...this.state.chatLog, container] }, () => { 159 | this.scrollToBottom(); 160 | }); 161 | } 162 | 163 | addSystemMessage(message) { 164 | const date = new Date().toLocaleTimeString([], { 165 | hour: "2-digit", 166 | minute: "2-digit", 167 | }); 168 | const element = ( 169 |
    170 | {message} ({date}) 171 |
    172 | ); 173 | 174 | const container = { 175 | class: "systemLog", 176 | element, 177 | }; 178 | this.setState({ chatLog: [...this.state.chatLog, container] }, () => { 179 | this.scrollToBottom(); 180 | }); 181 | } 182 | 183 | parseMessageText(inputString) { 184 | // If the message was a link to an image 185 | if (this.imageFile(this.getFileType(inputString)) === true) { 186 | // Build up the message as an image tag 187 | return this.imageLink(inputString); 188 | } 189 | 190 | // Return a span tag with the text of the message 191 | return {inputString}; 192 | } 193 | 194 | imageFile(filetype) { 195 | // Create an object that stores the image file formats 196 | const imageFormats = { 197 | jpg: 0, 198 | gif: 0, 199 | jpeg: 0, 200 | }; 201 | 202 | // Iterate through the list of image file formats 203 | for (const files in imageFormats) { 204 | // Check if the file type passed to the method is in our list of approved image file formats 205 | if (imageFormats.hasOwnProperty(filetype) === true) { 206 | return true; 207 | } 208 | } 209 | 210 | // If it's not an approved file format then return false. 211 | return false; 212 | } 213 | 214 | getFileType(inputString) { 215 | return inputString.split(".").pop(); 216 | } 217 | 218 | imageLink(inputString) { 219 | // Trim the input string 220 | inputString = inputString.trim(); 221 | 222 | // Get the file type from the input 223 | const filetype = this.getFileType(inputString); 224 | 225 | // Build up an image tag and return it 226 | // Set the source of the img tag 227 | if ( 228 | inputString.substring(0, 4) === "http" && 229 | this.imageFile(filetype) === true 230 | ) { 231 | return ( 232 | image_message 237 | ); 238 | } 239 | } 240 | 241 | scrollToBottom() { 242 | this.el.scrollIntoView({ behavior: "smooth", inline: "nearest" }); 243 | } 244 | 245 | notifyUser(message) { 246 | // Create a push notification 247 | Push.create(message.username, { 248 | body: message.message, 249 | timeout: 5000, 250 | icon: chatIcon, 251 | onClick() { 252 | window.focus(); 253 | this.close(); 254 | }, 255 | }); 256 | this.Sound.play(); 257 | } 258 | 259 | Logout() { 260 | localStorage.clear(); 261 | this.setState({ logout: true }); 262 | } 263 | 264 | Avatar(event) { 265 | this.setState( 266 | { 267 | avatar: URL.createObjectURL(event.target.files[0]), 268 | }, 269 | () => { 270 | this.state.socket.emit("add avatar", this.state.avatar); 271 | } 272 | ); 273 | } 274 | 275 | displayChat() { 276 | return ( 277 |
    278 |
    334 | ); 335 | } 336 | 337 | render() { 338 | let currentPage; 339 | if (this.state.logout) { 340 | currentPage = ; 341 | } else { 342 | currentPage = this.displayChat(); 343 | } 344 | return
    {currentPage}
    ; 345 | } 346 | } 347 | 348 | export default ChatPage; 349 | -------------------------------------------------------------------------------- /src/chatPage/test.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./chatPage.css"; 3 | import * as Push from "push.js"; 4 | 5 | class ChatPage extends Component { 6 | greetUserName() { 7 | // Create a span with the greeting message 8 | const userGreeting = ( 9 | Hello there{this.state.username}! 10 | ); // Create a container for the message text 11 | const greetingContainer =
  • {userGreeting}
  • ; 12 | // Add the greeting to t he chat log 13 | 14 | this.setState({ greeting: greetingContainer }); 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/chatPage/user.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OTRChat/NodeChat/7a574466c36e89f31e570bba8399be20fd44ef28/src/chatPage/user.png -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./app/App.js"; 4 | import registerServiceWorker from "./registerServiceWorker"; 5 | 6 | ReactDOM.render(, document.getElementById("root")); 7 | registerServiceWorker(); 8 | -------------------------------------------------------------------------------- /src/loginPage/loginPage.css: -------------------------------------------------------------------------------- 1 | .loginPage { 2 | background-color: #000; 3 | opacity: 0.7; } 4 | 5 | .loginForm { 6 | position: absolute; 7 | height: 100px; 8 | width: 100%; 9 | margin-top: -100px; 10 | text-align: center; 11 | top: 50%; } 12 | 13 | .title { 14 | color: #fff; 15 | font-size: 30px; } 16 | -------------------------------------------------------------------------------- /src/loginPage/loginPage.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from "react"; 2 | import "./loginPage.css"; 3 | import io from "socket.io-client"; 4 | import ChatPage from "../chatPage/chatPage.js"; 5 | 6 | class LoginPage extends Component { 7 | constructor(props) { 8 | super(props); 9 | 10 | this.socket = io("http://localhost:9876"); 11 | 12 | this.state = { 13 | currentInput: "", 14 | username: "", 15 | usernameInput: "", 16 | isLoggedIn: false, 17 | previousUser: false, 18 | socket: "", 19 | connected: false, 20 | }; 21 | this.setUsernameInput = this.setUsernameInput.bind(this); 22 | this.enterKeyPress = this.enterKeyPress.bind(this); 23 | } 24 | 25 | enterKeyPress(e) { 26 | if (e.keyCode === 13) { 27 | this.setUsername(); 28 | this.setState({ setUsernameInput: "" }); 29 | } 30 | } 31 | 32 | setUsernameInput(event) { 33 | this.setState({ usernameInput: event.target.value }); 34 | } 35 | 36 | componentDidMount() { 37 | this.checkIfPreviousUser(); 38 | } 39 | 40 | // Called when the user logs in 41 | // It sets the username global and opens the chat page for them 42 | setUsername() { 43 | // If the username is not blank (we don't want blank usernames) 44 | if (this.state.usernameInput) { 45 | // Get the username from the username input 46 | // callback forces username update 47 | this.setState({ username: this.state.usernameInput }, function() { 48 | // Hide the login page as the user has got a username now 49 | this.setState({ isLoggedIn: true }); 50 | 51 | // Tell the server that we have got a new user 52 | this.socket.emit("user join", this.state.username); 53 | 54 | // Set the username that the user chose in localstorage 55 | // This is so that when we reload the page, we can get the username they had previously 56 | localStorage.setItem("NodeChatUsername", this.state.username); 57 | 58 | // Set the connected global 59 | this.setState({ connected: true }); 60 | }); 61 | } 62 | } 63 | 64 | checkIfPreviousUser() { 65 | const previousUsername = localStorage.getItem("NodeChatUsername"); 66 | 67 | if (previousUsername !== null) { 68 | this.setState( 69 | { 70 | // Hide the login page 71 | isLoggedIn: true, 72 | // Set the global username variable 73 | username: previousUsername, 74 | // Set the global variable that the user is connected 75 | connected: true, 76 | previousUser: true, 77 | }, 78 | () => { 79 | this.userJoin(); 80 | } 81 | ); 82 | } 83 | // If the user did not have a username saved in localstorage then the chatPage is hidden 84 | // login page is shown, which ask for a username from the user. 85 | } 86 | 87 | // Tell the server that we've had a user join 88 | userJoin() { 89 | this.socket.emit("user join", this.state.username); 90 | } 91 | 92 | displayLogin() { 93 | return ( 94 |
    95 |
      96 |
    • 97 |
      98 |
      Please enter a Nickname
      99 | 107 |
      108 |
    • 109 |
    110 |
    111 | ); 112 | } 113 | 114 | render() { 115 | let currentPage; 116 | if (this.state.isLoggedIn) { 117 | currentPage = ( 118 | 124 | ); 125 | } else { 126 | currentPage = this.displayLogin(); 127 | } 128 | return
    {currentPage}
    ; 129 | } 130 | } 131 | 132 | export default LoginPage; 133 | -------------------------------------------------------------------------------- /src/registerServiceWorker.js: -------------------------------------------------------------------------------- 1 | // In production, we register a service worker to serve assets from local cache. 2 | 3 | // This lets the app load faster on subsequent visits in production, and gives 4 | // it offline capabilities. However, it also means that developers (and users) 5 | // will only see deployed updates on the "N+1" visit to a page, since previously 6 | // cached resources are updated in the background. 7 | 8 | // To learn more about the benefits of this model, read https://goo.gl/KwvDNy. 9 | // This link also includes instructions on opting out of this behavior. 10 | 11 | const isLocalhost = Boolean( 12 | window.location.hostname === "localhost" || 13 | // [::1] is the IPv6 localhost address. 14 | window.location.hostname === "[::1]" || 15 | // 127.0.0.1/8 is considered localhost for IPv4. 16 | window.location.hostname.match( 17 | /^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/ 18 | ) 19 | ); 20 | 21 | export default function register() { 22 | if (process.env.NODE_ENV === "production" && "serviceWorker" in navigator) { 23 | // The URL constructor is available in all browsers that support SW. 24 | const publicUrl = new URL(process.env.PUBLIC_URL, window.location); 25 | if (publicUrl.origin !== window.location.origin) { 26 | // Our service worker won't work if PUBLIC_URL is on a different origin 27 | // from what our page is served on. This might happen if a CDN is used to 28 | // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374 29 | return; 30 | } 31 | 32 | window.addEventListener("load", () => { 33 | const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`; 34 | 35 | if (isLocalhost) { 36 | // This is running on localhost. Lets check if a service worker still exists or not. 37 | checkValidServiceWorker(swUrl); 38 | 39 | // Add some additional logging to localhost, pointing developers to the 40 | // service worker/PWA documentation. 41 | navigator.serviceWorker.ready.then(() => { 42 | console.log( 43 | "This web app is being served cache-first by a service " + 44 | "worker. To learn more, visit https://goo.gl/SC7cgQ" 45 | ); 46 | }); 47 | } else { 48 | // Is not local host. Just register service worker 49 | registerValidSW(swUrl); 50 | } 51 | }); 52 | } 53 | } 54 | 55 | function registerValidSW(swUrl) { 56 | navigator.serviceWorker 57 | .register(swUrl) 58 | .then(registration => { 59 | registration.onupdatefound = () => { 60 | const installingWorker = registration.installing; 61 | installingWorker.onstatechange = () => { 62 | if (installingWorker.state === "installed") { 63 | if (navigator.serviceWorker.controller) { 64 | // At this point, the old content will have been purged and 65 | // the fresh content will have been added to the cache. 66 | // It's the perfect time to display a "New content is 67 | // available; please refresh." message in your web app. 68 | console.log("New content is available; please refresh."); 69 | } else { 70 | // At this point, everything has been precached. 71 | // It's the perfect time to display a 72 | // "Content is cached for offline use." message. 73 | console.log("Content is cached for offline use."); 74 | } 75 | } 76 | }; 77 | }; 78 | }) 79 | .catch(error => { 80 | console.error("Error during service worker registration:", error); 81 | }); 82 | } 83 | 84 | function checkValidServiceWorker(swUrl) { 85 | // Check if the service worker can be found. If it can't reload the page. 86 | fetch(swUrl) 87 | .then(response => { 88 | // Ensure service worker exists, and that we really are getting a JS file. 89 | if ( 90 | response.status === 404 || 91 | response.headers.get("content-type").indexOf("javascript") === -1 92 | ) { 93 | // No service worker found. Probably a different app. Reload the page. 94 | navigator.serviceWorker.ready.then(registration => { 95 | registration.unregister().then(() => { 96 | window.location.reload(); 97 | }); 98 | }); 99 | } else { 100 | // Service worker found. Proceed as normal. 101 | registerValidSW(swUrl); 102 | } 103 | }) 104 | .catch(() => { 105 | console.log( 106 | "No internet connection found. App is running in offline mode." 107 | ); 108 | }); 109 | } 110 | 111 | export function unregister() { 112 | if ("serviceWorker" in navigator) { 113 | navigator.serviceWorker.ready.then(registration => { 114 | registration.unregister(); 115 | }); 116 | } 117 | } 118 | --------------------------------------------------------------------------------