├── .gitignore ├── README.md ├── app ├── .gitignore ├── Dockerfile ├── README.md ├── docker-compose.yml ├── package.json ├── public │ ├── favicon.ico │ ├── index.html │ └── manifest.json ├── src │ ├── components │ │ ├── app.js │ │ ├── messenger.js │ │ ├── search-user.js │ │ ├── user-bar.js │ │ ├── user-form.js │ │ └── user-menu.js │ ├── config.js │ ├── css │ │ ├── .sass-cache │ │ │ └── c6c61f1d3471adfa6b4f36ea934cb8d28f43b0b7 │ │ │ │ ├── _font.scssc │ │ │ │ ├── _variable.scssc │ │ │ │ └── app.scssc │ │ ├── _font.scss │ │ ├── _variable.scss │ │ ├── app.css │ │ ├── app.css.map │ │ ├── app.scss │ │ └── fonts │ │ │ ├── chatapp.eot │ │ │ ├── chatapp.svg │ │ │ ├── chatapp.ttf │ │ │ └── chatapp.woff │ ├── helpers │ │ ├── index.js │ │ └── objectid.js │ ├── images │ │ └── avatar.png │ ├── index.js │ ├── realtime.js │ ├── registerServiceWorker.js │ ├── service.js │ └── store.js └── yarn.lock ├── deployment-to-digitalocean-hosting.md └── server ├── Dockerfile ├── docker-compose.yml ├── package.json └── src ├── app-router.js ├── database.js ├── helper.js ├── index.js ├── models ├── channel.js ├── connection.js ├── index.js ├── message.js ├── token.js └── user.js └── www └── index.html /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .DS_Store 3 | .env.local 4 | .env.development.local 5 | .env.test.local 6 | .env.production.local 7 | 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | 12 | server/node_modules 13 | app/node_modules 14 | server/dist 15 | server/package-lock.json 16 | 17 | app/node_modules 18 | app/package-lock.json -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-reactjs-chatapp 2 | 3 | Create messenger chat application use Nodejs Expressjs, Reactjs. 4 | 5 | ## Screenshot: 6 | 7 | 8 | 9 | ## Server 10 | 11 | ``` 12 | cd server 13 | ``` 14 | ``` 15 | npm install 16 | ``` 17 | 18 | ``` 19 | npm run dev 20 | ``` 21 | ### Reactjs App development 22 | 23 | ``` 24 | cd app 25 | ``` 26 | 27 | ``` 28 | npm start 29 | ``` 30 | 31 | ### Reactjs App development using docker-compose 32 | 33 | The docker-compose files are located in the two different application folders app and server. To run all the functions using docker run the follow commands: 34 | ``` 35 | cd server 36 | ``` 37 | ``` 38 | docker-compose up 39 | ``` 40 | At this moment the server application side will be running. 41 | 42 | Now it's time to run application front end. Open a new terminal (window or tab) and in the project folder use the following commands: 43 | ``` 44 | cd app 45 | ``` 46 | ``` 47 | docker-compose up 48 | ``` 49 | 50 | Attention: Deppending on the way you have installed the docker in your compile you may use **sudo** command to run docker, for example: 51 | ``` 52 | sudo docker-compose up 53 | ``` 54 | 55 | For more docker informations and how to install access https://www.docker.com/ . 56 | 57 | ## Tutorials 58 | * Checkout the video toturials list: https://www.youtube.com/playlist?list=PLFaW_8zE4amPaLyz5AyVT8B_wfOYwd8x8 59 | * My Facebook: https://www.facebook.com/TabvnGroup/ 60 | * Youtube Chanel: https://youtube.com/tabvn 61 | 62 | 63 | ## Deploy Node.js React.js to DigitalOcean.com Ubuntu 16.04 Cloud VPS 64 | 65 | * Document 66 | * Video: https://www.youtube.com/watch?v=wJsH45eWNBo 67 | 68 | -------------------------------------------------------------------------------- /app/.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 | -------------------------------------------------------------------------------- /app/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:11.12.0 2 | 3 | # Install a bunch of node modules that are commonly used. 4 | #ADD package.json /usr/app/ 5 | ADD . /usr/app/ 6 | 7 | EXPOSE 80 8 | ENV BIND_HOST=0.0.0.0 9 | CMD ["npm", "start"] 10 | WORKDIR /usr/app 11 | 12 | RUN npm install 13 | -------------------------------------------------------------------------------- /app/README.md: -------------------------------------------------------------------------------- 1 | ## Start app 2 | 3 | ``` 4 | npm install 5 | ``` 6 | 7 | ``` 8 | npm start 9 | ``` -------------------------------------------------------------------------------- /app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: '3' 2 | services: 3 | app: 4 | build: . 5 | ports: 6 | - "3000:3000" 7 | command: npm start -------------------------------------------------------------------------------- /app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-app", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "axios": "^0.17.1", 7 | "classnames": "^2.2.5", 8 | "immutable": "^3.8.2", 9 | "lodash": "^4.17.4", 10 | "moment": "^2.19.2", 11 | "react": "^16.1.1", 12 | "react-dom": "^16.1.1", 13 | "react-scripts": "1.0.17" 14 | }, 15 | "scripts": { 16 | "start": "react-scripts start", 17 | "build": "react-scripts build", 18 | "test": "react-scripts test --env=jsdom", 19 | "eject": "react-scripts eject" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /app/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/tabvn/nodejs-reactjs-chatapp/4ff49c67588594195f09ae83b9b03e837813e0d4/app/public/favicon.ico -------------------------------------------------------------------------------- /app/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 11 | 12 | 13 | 22 | React App 23 | 24 | 25 | 28 |
29 | 39 | 40 | 41 | -------------------------------------------------------------------------------- /app/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 | -------------------------------------------------------------------------------- /app/src/components/app.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import Store from '../store' 3 | import Messenger from './messenger' 4 | 5 | export default class App extends Component{ 6 | 7 | constructor(props){ 8 | super(props); 9 | 10 | this.state = { 11 | 12 | store: new Store(this), 13 | } 14 | } 15 | 16 | render(){ 17 | 18 | const {store} = this.state; 19 | return
20 | 21 |
22 | } 23 | } -------------------------------------------------------------------------------- /app/src/components/messenger.js: -------------------------------------------------------------------------------- 1 | import React, {Component} from 'react' 2 | import classNames from 'classnames' 3 | import {OrderedMap} from 'immutable' 4 | import _ from 'lodash' 5 | import {ObjectID} from '../helpers/objectid' 6 | import SearchUser from './search-user' 7 | import moment from 'moment' 8 | import UserBar from './user-bar' 9 | 10 | 11 | export default class Messenger extends Component { 12 | 13 | constructor(props) { 14 | 15 | super(props); 16 | 17 | this.state = { 18 | height: window.innerHeight, 19 | newMessage: 'Hello there...', 20 | searchUser: "", 21 | showSearchUser: false, 22 | } 23 | 24 | this._onResize = this._onResize.bind(this); 25 | this.handleSend = this.handleSend.bind(this) 26 | this.renderMessage = this.renderMessage.bind(this); 27 | this.scrollMessagesToBottom = this.scrollMessagesToBottom.bind(this) 28 | this._onCreateChannel = this._onCreateChannel.bind(this); 29 | this.renderChannelTitle = this.renderChannelTitle.bind(this) 30 | this.renderChannelAvatars = this.renderChannelAvatars.bind(this); 31 | } 32 | 33 | renderChannelAvatars(channel){ 34 | const {store} = this.props; 35 | 36 | const members = store.getMembersFromChannel(channel); 37 | 38 | const maxDisplay = 4; 39 | const total = members.size > maxDisplay ? maxDisplay : members.size; 40 | 41 | const avatars = members.map((user, index) => { 42 | 43 | 44 | 45 | return index < maxDisplay ? {_.get(user, : null 46 | 47 | }); 48 | 49 | 50 | return
{avatars}
51 | } 52 | renderChannelTitle(channel = null) { 53 | 54 | if (!channel) { 55 | return null; 56 | } 57 | const {store} = this.props; 58 | 59 | const members = store.getMembersFromChannel(channel); 60 | 61 | 62 | const names = []; 63 | 64 | members.forEach((user) => { 65 | 66 | const name = _.get(user, 'name'); 67 | names.push(name); 68 | }) 69 | 70 | let title = _.join(names, ','); 71 | 72 | if (!title && _.get(channel, 'isNew')) { 73 | title = 'New message'; 74 | } 75 | 76 | return

{title}

77 | } 78 | 79 | _onCreateChannel() { 80 | 81 | const {store} = this.props; 82 | 83 | const currentUser = store.getCurrentUser(); 84 | const currentUserId = _.get(currentUser, '_id'); 85 | 86 | const channelId = new ObjectID().toString(); 87 | const channel = { 88 | _id: channelId, 89 | title: '', 90 | lastMessage: "", 91 | members: new OrderedMap(), 92 | messages: new OrderedMap(), 93 | isNew: true, 94 | userId: currentUserId, 95 | created: new Date(), 96 | }; 97 | 98 | channel.members = channel.members.set(currentUserId, true); 99 | 100 | 101 | store.onCreateNewChannel(channel); 102 | 103 | 104 | } 105 | 106 | scrollMessagesToBottom() { 107 | 108 | if (this.messagesRef) { 109 | 110 | this.messagesRef.scrollTop = this.messagesRef.scrollHeight; 111 | } 112 | } 113 | 114 | renderMessage(message) { 115 | 116 | const text = _.get(message, 'body', ''); 117 | 118 | const html = _.split(text, '\n').map((m, key) => { 119 | 120 | return

121 | }) 122 | 123 | 124 | return html; 125 | } 126 | 127 | handleSend() { 128 | 129 | const {newMessage} = this.state; 130 | const {store} = this.props; 131 | 132 | 133 | // create new message 134 | 135 | if (_.trim(newMessage).length) { 136 | 137 | const messageId = new ObjectID().toString(); 138 | const channel = store.getActiveChannel(); 139 | const channelId = _.get(channel, '_id', null); 140 | const currentUser = store.getCurrentUser(); 141 | 142 | const message = { 143 | _id: messageId, 144 | channelId: channelId, 145 | body: newMessage, 146 | userId: _.get(currentUser, '_id'), 147 | me: true, 148 | 149 | }; 150 | 151 | 152 | store.addMessage(messageId, message); 153 | 154 | this.setState({ 155 | newMessage: '', 156 | }) 157 | } 158 | 159 | 160 | } 161 | 162 | _onResize() { 163 | 164 | this.setState({ 165 | height: window.innerHeight 166 | }); 167 | } 168 | 169 | componentDidUpdate() { 170 | 171 | 172 | this.scrollMessagesToBottom(); 173 | } 174 | 175 | componentDidMount() { 176 | 177 | 178 | window.addEventListener('resize', this._onResize); 179 | 180 | 181 | } 182 | 183 | 184 | componentWillUnmount() { 185 | 186 | window.removeEventListener('resize', this._onResize) 187 | 188 | } 189 | 190 | render() { 191 | 192 | const {store} = this.props; 193 | 194 | const {height} = this.state; 195 | 196 | const style = { 197 | height: height, 198 | }; 199 | 200 | 201 | const activeChannel = store.getActiveChannel(); 202 | const messages = store.getMessagesFromChannel(activeChannel); //store.getMessages(); 203 | const channels = store.getChannels(); 204 | const members = store.getMembersFromChannel(activeChannel); 205 | 206 | 207 | return ( 208 |

209 |
210 |
211 | 212 | 214 |

Messenger

215 |
216 |
217 | 218 | {_.get(activeChannel, 'isNew') ?
219 | 220 | { 221 | members.map((user, key) => { 222 | 223 | return { 224 | 225 | store.removeMemberFromChannel(activeChannel, user); 226 | 227 | }} key={key}>{_.get(user, 'name')} 228 | }) 229 | } 230 | { 231 | 232 | const searchUserText = _.get(event, 'target.value'); 233 | 234 | //console.log("searching for user with name: ", searchUserText) 235 | 236 | this.setState({ 237 | searchUser: searchUserText, 238 | showSearchUser: true, 239 | }, () => { 240 | 241 | 242 | store.startSearchUsers(searchUserText); 243 | }); 244 | 245 | 246 | }} type="text" value={this.state.searchUser}/> 247 | 248 | {this.state.showSearchUser ? { 250 | 251 | this.setState({ 252 | showSearchUser: false, 253 | searchUser: '', 254 | 255 | }, () => { 256 | 257 | 258 | const userId = _.get(user, '_id'); 259 | const channelId = _.get(activeChannel, '_id'); 260 | 261 | store.addUserToChannel(channelId, userId); 262 | 263 | }); 264 | 265 | 266 | }} 267 | store={store}/> : null} 268 | 269 |
: this.renderChannelTitle(activeChannel)} 270 | 271 | 272 |
273 |
274 | 275 | 276 | 277 |
278 |
279 |
280 |
281 | 282 |
283 | 284 | {channels.map((channel, key) => { 285 | 286 | return ( 287 |
{ 288 | 289 | store.setActiveChannelId(channel._id); 290 | 291 | }} key={channel._id} 292 | className={classNames('chanel', {'notify': _.get(channel, 'notify') === true},{'active': _.get(activeChannel, '_id') === _.get(channel, '_id', null)})}> 293 |
294 | {this.renderChannelAvatars(channel)} 295 |
296 |
297 | {this.renderChannelTitle(channel)} 298 |

{channel.lastMessage}

299 |
300 | 301 |
302 | ) 303 | 304 | })} 305 | 306 | 307 |
308 |
309 |
310 |
this.messagesRef = ref} className="messages"> 311 | 312 | {messages.map((message, index) => { 313 | 314 | const user = _.get(message, 'user'); 315 | 316 | 317 | return ( 318 |
319 |
320 | 321 |
322 |
323 |
{message.me ? 'You ' : _.get(message, 'user.name')} says: 325 |
326 |
327 | {this.renderMessage(message)} 328 |
329 |
330 |
331 | ) 332 | 333 | 334 | })} 335 | 336 | 337 |
338 | 339 | {activeChannel && members.size > 0 ?
340 | 341 |
342 |