Repository: tabvn/nodejs-reactjs-chatapp
Branch: master
Commit: 4ff49c675885
Files: 45
Total size: 131.9 KB
Directory structure:
gitextract_xpe_08_e/
├── .gitignore
├── README.md
├── app/
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── docker-compose.yml
│ ├── package.json
│ ├── public/
│ │ ├── 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.scss
│ ├── helpers/
│ │ ├── index.js
│ │ └── objectid.js
│ ├── index.js
│ ├── realtime.js
│ ├── registerServiceWorker.js
│ ├── service.js
│ └── store.js
├── 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
================================================
FILE CONTENTS
================================================
================================================
FILE: .gitignore
================================================
.idea
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
server/node_modules
app/node_modules
server/dist
server/package-lock.json
app/node_modules
app/package-lock.json
================================================
FILE: README.md
================================================
# nodejs-reactjs-chatapp
Create messenger chat application use Nodejs Expressjs, Reactjs.
## Screenshot:
<img src="https://lh3.googleusercontent.com/bk7OOm_rDDP8TgKK3KYj5lEVBc4FptkWBlGce6_pRjBj2TMTSQD6jgTdxyU0vqI30AaacSntUuhzkiltph_jMJYI4bUrjN3AVcoyDp-HC0aR-iXZ_zoLhR9cfeI9gdifcnPp8TlRpQ=w2548-h1318-no" />
## Server
```
cd server
```
```
npm install
```
```
npm run dev
```
### Reactjs App development
```
cd app
```
```
npm start
```
### Reactjs App development using docker-compose
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:
```
cd server
```
```
docker-compose up
```
At this moment the server application side will be running.
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:
```
cd app
```
```
docker-compose up
```
Attention: Deppending on the way you have installed the docker in your compile you may use **sudo** command to run docker, for example:
```
sudo docker-compose up
```
For more docker informations and how to install access https://www.docker.com/ .
## Tutorials
* Checkout the video toturials list: https://www.youtube.com/playlist?list=PLFaW_8zE4amPaLyz5AyVT8B_wfOYwd8x8
* My Facebook: https://www.facebook.com/TabvnGroup/
* Youtube Chanel: https://youtube.com/tabvn
## Deploy Node.js React.js to DigitalOcean.com Ubuntu 16.04 Cloud VPS
* <a href="https://github.com/tabvn/nodejs-reactjs-chatapp/blob/master/deployment-to-digitalocean-hosting.md">Document</a>
* Video: https://www.youtube.com/watch?v=wJsH45eWNBo
================================================
FILE: app/.gitignore
================================================
# See https://help.github.com/ignore-files/ for more about ignoring files.
# dependencies
/node_modules
# testing
/coverage
# production
/build
# misc
.DS_Store
.env.local
.env.development.local
.env.test.local
.env.production.local
npm-debug.log*
yarn-debug.log*
yarn-error.log*
================================================
FILE: app/Dockerfile
================================================
FROM node:11.12.0
# Install a bunch of node modules that are commonly used.
#ADD package.json /usr/app/
ADD . /usr/app/
EXPOSE 80
ENV BIND_HOST=0.0.0.0
CMD ["npm", "start"]
WORKDIR /usr/app
RUN npm install
================================================
FILE: app/README.md
================================================
## Start app
```
npm install
```
```
npm start
```
================================================
FILE: app/docker-compose.yml
================================================
version: '3'
services:
app:
build: .
ports:
- "3000:3000"
command: npm start
================================================
FILE: app/package.json
================================================
{
"name": "my-app",
"version": "0.1.0",
"private": true,
"dependencies": {
"axios": "^0.17.1",
"classnames": "^2.2.5",
"immutable": "^3.8.2",
"lodash": "^4.17.4",
"moment": "^2.19.2",
"react": "^16.1.1",
"react-dom": "^16.1.1",
"react-scripts": "1.0.17"
},
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build",
"test": "react-scripts test --env=jsdom",
"eject": "react-scripts eject"
}
}
================================================
FILE: app/public/index.html
================================================
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="theme-color" content="#000000">
<!--
manifest.json provides metadata used when your web app is added to the
homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/
-->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json">
<link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">
<!--
Notice the use of %PUBLIC_URL% in the tags above.
It will be replaced with the URL of the `public` folder during the build.
Only files inside the `public` folder can be referenced from the HTML.
Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
work correctly both with client-side routing and a non-root public URL.
Learn how to configure a non-root public URL by running `npm run build`.
-->
<title>React App</title>
</head>
<body>
<noscript>
You need to enable JavaScript to run this app.
</noscript>
<div id="root"></div>
<!--
This HTML file is a template.
If you open it directly in the browser, you will see an empty page.
You can add webfonts, meta tags, or analytics to this file.
The build step will place the bundled scripts into the <body> tag.
To begin the development, run `npm start` or `yarn start`.
To create a production bundle, use `npm run build` or `yarn build`.
-->
</body>
</html>
================================================
FILE: app/public/manifest.json
================================================
{
"short_name": "React App",
"name": "Create React App Sample",
"icons": [
{
"src": "favicon.ico",
"sizes": "64x64 32x32 24x24 16x16",
"type": "image/x-icon"
}
],
"start_url": "./index.html",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff"
}
================================================
FILE: app/src/components/app.js
================================================
import React, {Component} from 'react'
import Store from '../store'
import Messenger from './messenger'
export default class App extends Component{
constructor(props){
super(props);
this.state = {
store: new Store(this),
}
}
render(){
const {store} = this.state;
return <div className="app-wrapper">
<Messenger store={store} />
</div>
}
}
================================================
FILE: app/src/components/messenger.js
================================================
import React, {Component} from 'react'
import classNames from 'classnames'
import {OrderedMap} from 'immutable'
import _ from 'lodash'
import {ObjectID} from '../helpers/objectid'
import SearchUser from './search-user'
import moment from 'moment'
import UserBar from './user-bar'
export default class Messenger extends Component {
constructor(props) {
super(props);
this.state = {
height: window.innerHeight,
newMessage: 'Hello there...',
searchUser: "",
showSearchUser: false,
}
this._onResize = this._onResize.bind(this);
this.handleSend = this.handleSend.bind(this)
this.renderMessage = this.renderMessage.bind(this);
this.scrollMessagesToBottom = this.scrollMessagesToBottom.bind(this)
this._onCreateChannel = this._onCreateChannel.bind(this);
this.renderChannelTitle = this.renderChannelTitle.bind(this)
this.renderChannelAvatars = this.renderChannelAvatars.bind(this);
}
renderChannelAvatars(channel){
const {store} = this.props;
const members = store.getMembersFromChannel(channel);
const maxDisplay = 4;
const total = members.size > maxDisplay ? maxDisplay : members.size;
const avatars = members.map((user, index) => {
return index < maxDisplay ? <img key={index} src={_.get(user, 'avatar')} alt={_.get(user, 'name')} /> : null
});
return <div className={classNames('channel-avatars', `channel-avatars-${total}`)}>{avatars}</div>
}
renderChannelTitle(channel = null) {
if (!channel) {
return null;
}
const {store} = this.props;
const members = store.getMembersFromChannel(channel);
const names = [];
members.forEach((user) => {
const name = _.get(user, 'name');
names.push(name);
})
let title = _.join(names, ',');
if (!title && _.get(channel, 'isNew')) {
title = 'New message';
}
return <h2>{title}</h2>
}
_onCreateChannel() {
const {store} = this.props;
const currentUser = store.getCurrentUser();
const currentUserId = _.get(currentUser, '_id');
const channelId = new ObjectID().toString();
const channel = {
_id: channelId,
title: '',
lastMessage: "",
members: new OrderedMap(),
messages: new OrderedMap(),
isNew: true,
userId: currentUserId,
created: new Date(),
};
channel.members = channel.members.set(currentUserId, true);
store.onCreateNewChannel(channel);
}
scrollMessagesToBottom() {
if (this.messagesRef) {
this.messagesRef.scrollTop = this.messagesRef.scrollHeight;
}
}
renderMessage(message) {
const text = _.get(message, 'body', '');
const html = _.split(text, '\n').map((m, key) => {
return <p key={key} dangerouslySetInnerHTML={{__html: m}}/>
})
return html;
}
handleSend() {
const {newMessage} = this.state;
const {store} = this.props;
// create new message
if (_.trim(newMessage).length) {
const messageId = new ObjectID().toString();
const channel = store.getActiveChannel();
const channelId = _.get(channel, '_id', null);
const currentUser = store.getCurrentUser();
const message = {
_id: messageId,
channelId: channelId,
body: newMessage,
userId: _.get(currentUser, '_id'),
me: true,
};
store.addMessage(messageId, message);
this.setState({
newMessage: '',
})
}
}
_onResize() {
this.setState({
height: window.innerHeight
});
}
componentDidUpdate() {
this.scrollMessagesToBottom();
}
componentDidMount() {
window.addEventListener('resize', this._onResize);
}
componentWillUnmount() {
window.removeEventListener('resize', this._onResize)
}
render() {
const {store} = this.props;
const {height} = this.state;
const style = {
height: height,
};
const activeChannel = store.getActiveChannel();
const messages = store.getMessagesFromChannel(activeChannel); //store.getMessages();
const channels = store.getChannels();
const members = store.getMembersFromChannel(activeChannel);
return (
<div style={style} className="app-messenger">
<div className="header">
<div className="left">
<button className="left-action"><i className="icon-settings-streamline-1"/></button>
<button onClick={this._onCreateChannel} className="right-action"><i
className="icon-edit-modify-streamline"/></button>
<h2>Messenger</h2>
</div>
<div className="content">
{_.get(activeChannel, 'isNew') ? <div className="toolbar">
<label>To:</label>
{
members.map((user, key) => {
return <span onClick={() => {
store.removeMemberFromChannel(activeChannel, user);
}} key={key}>{_.get(user, 'name')}</span>
})
}
<input placeholder="Type name of person..." onChange={(event) => {
const searchUserText = _.get(event, 'target.value');
//console.log("searching for user with name: ", searchUserText)
this.setState({
searchUser: searchUserText,
showSearchUser: true,
}, () => {
store.startSearchUsers(searchUserText);
});
}} type="text" value={this.state.searchUser}/>
{this.state.showSearchUser ? <SearchUser
onSelect={(user) => {
this.setState({
showSearchUser: false,
searchUser: '',
}, () => {
const userId = _.get(user, '_id');
const channelId = _.get(activeChannel, '_id');
store.addUserToChannel(channelId, userId);
});
}}
store={store}/> : null}
</div> : this.renderChannelTitle(activeChannel)}
</div>
<div className="right">
<UserBar store={store}/>
</div>
</div>
<div className="main">
<div className="sidebar-left">
<div className="chanels">
{channels.map((channel, key) => {
return (
<div onClick={(key) => {
store.setActiveChannelId(channel._id);
}} key={channel._id}
className={classNames('chanel', {'notify': _.get(channel, 'notify') === true},{'active': _.get(activeChannel, '_id') === _.get(channel, '_id', null)})}>
<div className="user-image">
{this.renderChannelAvatars(channel)}
</div>
<div className="chanel-info">
{this.renderChannelTitle(channel)}
<p>{channel.lastMessage}</p>
</div>
</div>
)
})}
</div>
</div>
<div className="content">
<div ref={(ref) => this.messagesRef = ref} className="messages">
{messages.map((message, index) => {
const user = _.get(message, 'user');
return (
<div key={index} className={classNames('message', {'me': message.me})}>
<div className="message-user-image">
<img src={_.get(user, 'avatar')} alt=""/>
</div>
<div className="message-body">
<div
className="message-author">{message.me ? 'You ' : _.get(message, 'user.name')} says:
</div>
<div className="message-text">
{this.renderMessage(message)}
</div>
</div>
</div>
)
})}
</div>
{activeChannel && members.size > 0 ? <div className="messenger-input">
<div className="text-input">
<textarea onKeyUp={(event) => {
if (event.key === 'Enter' && !event.shiftKey) {
this.handleSend();
}
}} onChange={(event) => {
this.setState({newMessage: _.get(event, 'target.value')});
}} value={this.state.newMessage} placeholder="Write your messsage..."/>
</div>
<div className="actions">
<button onClick={this.handleSend} className="send">Send</button>
</div>
</div> : null}
</div>
<div className="sidebar-right">
{members.size > 0 ? <div><h2 className="title">Members</h2>
<div className="members">
{members.map((member, key) => {
const isOnline = _.get(member, 'online', false);
return (
<div key={key} className="member">
<div className="user-image">
<img src={_.get(member, 'avatar')} alt=""/>
<span className={classNames('user-status', {'online': isOnline})} />
</div>
<div className="member-info">
<h2>{member.name} - <span className={classNames('user-status', {'online': isOnline})}>{isOnline ? 'Online': 'Offline'}</span> </h2>
<p>Joined: {moment(member.created).fromNow()}</p>
</div>
</div>
)
})}
</div>
</div> : null}
</div>
</div>
</div>
)
}
}
================================================
FILE: app/src/components/search-user.js
================================================
import React, {Component} from 'react'
import _ from 'lodash'
export default class SearchUser extends Component{
constructor(props){
super(props);
this.handleOnClick = this.handleOnClick.bind(this);
}
handleOnClick(user){
if(this.props.onSelect){
this.props.onSelect(user);
}
}
render(){
const {store} = this.props;
const users = store.getSearchUsers();
return <div className="search-user">
<div className="user-list">
{users.map((user, index) => {
return (<div onClick={() => this.handleOnClick(user)} key={index} className="user">
<img src={_.get(user, 'avatar')} alt="..." />
<h2>{_.get(user, 'name')}</h2>
</div>)
})}
</div>
</div>
}
}
================================================
FILE: app/src/components/user-bar.js
================================================
import React, {Component} from 'react'
import _ from 'lodash'
import avatar from '../images/avatar.png'
import UserForm from './user-form'
import UserMenu from './user-menu'
export default class UserBar extends Component {
constructor(props) {
super(props);
this.state = {
showUserForm: false,
showUserMenu: false,
}
}
render() {
const {store} = this.props;
const me = store.getCurrentUser();
const profilePicture = _.get(me, 'avatar');
const isConnected = store.isConnected();
return (
<div className="user-bar">
{me && !isConnected ? <div className="app-warning-state">Reconnecting... </div> : null}
{!me ? <button onClick={() => {
this.setState({
showUserForm: true,
})
}} type="button" className="login-btn">Sign In</button> : null}
<div className="profile-name">{_.get(me, 'name')}</div>
<div className="profile-image" onClick={() => {
this.setState({
showUserMenu: true,
})
}}><img src={profilePicture ? profilePicture : avatar} alt=""/></div>
{!me && this.state.showUserForm ? <UserForm onClose={(msg) => {
this.setState({
showUserForm: false,
})
}} store={store}/> : null}
{this.state.showUserMenu ? <UserMenu
store={store}
onClose={() => {
this.setState({
showUserMenu: false,
})
}}
/> : null}
</div>
);
}
}
================================================
FILE: app/src/components/user-form.js
================================================
import React, {Component} from 'react'
import _ from 'lodash'
import classNames from 'classnames'
export default class UserForm extends Component {
constructor(props) {
super(props);
this.state = {
message: null,
isLogin: true,
user: {
name: '',
email: '',
password: ''
}
}
this.onSubmit = this.onSubmit.bind(this);
this.onTextFieldChange = this.onTextFieldChange.bind(this)
this.onClickOutside = this.onClickOutside.bind(this);
}
onClickOutside(event) {
if (this.ref && !this.ref.contains(event.target)) {
if (this.props.onClose) {
this.props.onClose();
}
}
}
componentDidMount() {
window.addEventListener('mousedown', this.onClickOutside);
}
componentWillUnmount() {
window.removeEventListener('mousedown', this.onClickOutside);
}
onSubmit(event) {
const {user, isLogin} = this.state;
const {store} = this.props;
event.preventDefault();
this.setState({
message: null,
}, () => {
if(isLogin){
store.login(user.email, user.password).then((user) => {
if (this.props.onClose) {
this.props.onClose();
}
}).catch((err) => {
console.log("err", err);
this.setState({
message: {
body: err,
type: 'error',
}
})
});
}else{
store.register(user).then((_)=> {
this.setState({
message: {
body: 'User created.',
type: 'success'
}
}, () => {
// now login this user
store.login(user.email, user.password).then(() => {
if (this.props.onClose) {
this.props.onClose();
}
})
})
})
}
})
}
onTextFieldChange(event) {
let {user} = this.state;
const field = event.target.name;
user[field] = event.target.value;
this.setState({
user: user
});
}
render() {
const {user, message, isLogin} = this.state;
return (
<div className="user-form" ref={(ref) => this.ref = ref}>
<form onSubmit={this.onSubmit} method="post">
{message ?
<p className={classNames('app-message', _.get(message, 'type'))}>{_.get(message, 'body')}</p> : null}
{!isLogin ? <div className="form-item">
<label>Name</label>
<input placeholder={'Full name'} onChange={this.onTextFieldChange} type={'text'} value={_.get(user, 'name', '')} name={"name"} />
</div> : null }
<div className="form-item">
<label>Email</label>
<input value={_.get(user, 'email')} onChange={this.onTextFieldChange} type="email"
placeholder="Email addresss..." name="email"/>
</div>
<div className="form-item">
<label>Password</label>
<input value={_.get(user, 'password')} onChange={this.onTextFieldChange} type="password"
placeholder="Password" name="password"/>
</div>
<div className="form-actions">
{isLogin ? <button onClick={() => {
this.setState({
isLogin: false,
})
}} type="button">Create an account?
</button> : null}
<button className="primary" type="submit">{isLogin ? 'Sign In' : 'Create new account'}</button>
</div>
</form>
</div>
);
}
}
================================================
FILE: app/src/components/user-menu.js
================================================
import React,{Component} from 'react'
export default class UserMenu extends Component{
constructor(props){
super(props);
this.onClickOutside = this.onClickOutside.bind(this);
}
onClickOutside(event){
if(this.ref && !this.ref.contains(event.target)){
if(this.props.onClose){
this.props.onClose();
}
}
}
componentDidMount(){
window.addEventListener('mousedown', this.onClickOutside);
}
componentWillUnmount(){
window.removeEventListener('mousedown', this.onClickOutside);
}
render(){
const {store} = this.props;
const user = store.getCurrentUser();
return <div className="user-menu" ref={(ref) => this.ref = ref}>
{user ? <div>
<h2>My menu</h2>
<ul className="menu">
<li><button onClick={() => {
if(this.props.onClose){
this.props.onClose();
}
store.signOut();
}} type="button">Sign Out</button></li>
</ul>
</div> : null }
</div>
}
}
================================================
FILE: app/src/config.js
================================================
export const production = false; // set it to true when deploy to the server
const domain = production ? '139.59.227.127' : '127.0.0.1:3001'; // if you have domain pointed to digitalOcean Cloud server let use your domain.eg: tabvn.com
export const websocketUrl = `ws://${domain}`
export const apiUrl = `http://${domain}`
================================================
FILE: app/src/css/_font.scss
================================================
@charset "UTF-8";
@font-face {
font-family: "chatapp";
src:url("./fonts/chatapp.eot");
src:url("./fonts/chatapp.eot?#iefix") format("embedded-opentype"),
url("./fonts/chatapp.woff") format("woff"),
url("./fonts/chatapp.ttf") format("truetype"),
url("./fonts/chatapp.svg#chatapp") format("svg");
font-weight: normal;
font-style: normal;
}
[data-icon]:before {
font-family: "chatapp" !important;
content: attr(data-icon);
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
text-transform: none !important;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
[class^="icon-"]:before,
[class*=" icon-"]:before {
font-family: "chatapp" !important;
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
text-transform: none !important;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
.icon-edit-modify-streamline:before {
content: "\61";
}
.icon-settings-streamline-1:before {
content: "\63";
}
.icon-paperplane:before {
content: "\62";
}
================================================
FILE: app/src/css/_variable.scss
================================================
$header-height: 50px;
$left-sidebar-width: 200px;
$right-sidebar-width: 300px;
$border-color: rgba(0, 0, 0, 0.05);
$primary-color: #2ecc71;
$danger-color: #e74c3c;
$body-color: #2c3e50;
================================================
FILE: app/src/css/app.css
================================================
@import "https://fonts.googleapis.com/css?family=Open+Sans:400,600";
@font-face {
font-family: "chatapp";
src: url("./fonts/chatapp.eot");
src: url("./fonts/chatapp.eot?#iefix") format("embedded-opentype"), url("./fonts/chatapp.woff") format("woff"), url("./fonts/chatapp.ttf") format("truetype"), url("./fonts/chatapp.svg#chatapp") format("svg");
font-weight: normal;
font-style: normal; }
[data-icon]:before {
font-family: "chatapp" !important;
content: attr(data-icon);
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
text-transform: none !important;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }
[class^="icon-"]:before,
[class*=" icon-"]:before {
font-family: "chatapp" !important;
font-style: normal !important;
font-weight: normal !important;
font-variant: normal !important;
text-transform: none !important;
speak: none;
line-height: 1;
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale; }
.icon-edit-modify-streamline:before {
content: "\61"; }
.icon-settings-streamline-1:before {
content: "\63"; }
.icon-paperplane:before {
content: "\62"; }
body, html {
margin: 0;
padding: 0;
height: 100%; }
body {
color: #2c3e50;
font-size: 13px;
font-family: 'Open Sans', sans-serif; }
* {
box-sizing: border-box;
padding: 0;
margin: 0; }
.app-messenger {
display: flex;
flex-direction: column; }
.app-messenger .header {
height: 50px;
display: flex;
flex-direction: row;
border-bottom: 1px solid rgba(0, 0, 0, 0.05); }
.app-messenger .header .left {
width: 200px;
position: relative; }
.app-messenger .header .left .left-action {
position: absolute;
left: 8px;
top: 0; }
.app-messenger .header .left .right-action {
position: absolute;
right: 8px;
top: 0; }
.app-messenger .header .left h2 {
line-height: 50px;
font-size: 14px;
font-weight: 600;
display: block;
text-align: center; }
.app-messenger .header .left button {
background: none;
line-height: 50px;
border: 0 none;
font-size: 20px;
cursor: pointer; }
.app-messenger .header .content {
flex-grow: 1; }
.app-messenger .header .content h2 {
line-height: 50px;
text-align: center; }
.app-messenger .header .right {
width: 300px; }
.app-messenger .header .right .user-bar {
line-height: 50px;
display: flex;
justify-content: flex-end;
padding: 0 10px; }
.app-messenger .header .right .user-bar .profile-name {
padding-right: 10px; }
.app-messenger .header .right .user-bar .profile-image {
line-height: 50px; }
.app-messenger .header .right .user-bar .profile-image img {
width: 30px;
height: 30px;
border-radius: 50%;
margin: 10px 0 0 0; }
.app-messenger .main {
height: 100%;
display: flex;
overflow: hidden; }
.app-messenger .main .sidebar-left {
width: 200px;
border-right: 1px solid rgba(0, 0, 0, 0.05); }
.app-messenger .main .sidebar-right {
border-left: 1px solid rgba(0, 0, 0, 0.05);
width: 300px; }
.app-messenger .main .sidebar-right .title {
padding: 10px; }
.app-messenger .main .content {
flex-grow: 1;
overflow: hidden;
display: flex;
flex-direction: column; }
.app-messenger .main .content .messages {
flex-grow: 1; }
.app-messenger .main .content .messenger-input {
border-top: 1px solid rgba(0, 0, 0, 0.05);
height: 50px;
display: flex;
flex-direction: row; }
.app-messenger .main .content .messenger-input .text-input {
flex-grow: 1; }
.app-messenger .main .content .messenger-input .text-input textarea {
border: 0 none;
width: 100%;
height: 100%;
padding: 8px 15px; }
.app-messenger .main .content .messenger-input .actions button.send {
background: #2ecc71;
color: #FFF;
border: 0 none;
padding: 7px 15px;
line-height: 50px; }
.messages {
display: flex;
flex-direction: column;
overflow-y: auto;
height: 100%; }
.messages .message {
display: flex;
flex-direction: row;
justify-content: flex-start;
margin: 15px; }
.messages .message .message-user-image img {
width: 20px;
height: 20px;
border-radius: 50%; }
.messages .message .message-body {
padding-left: 10px; }
.messages .message .message-body .message-text {
background: rgba(0, 0, 0, 0.05);
padding: 10px;
border-radius: 10px; }
.messages .message.me {
justify-content: flex-end; }
.messages .message.me .message-body .message-text {
background: #2ecc71;
color: #FFF; }
.chanels {
overflow-y: auto;
height: 100%; }
.chanels .chanel {
cursor: pointer;
display: flex;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding: 8px; }
.chanels .chanel .user-image {
width: 30px; }
.chanels .chanel .user-image img {
max-width: 100%; }
.chanels .chanel .user-image .channel-avatars {
overflow: hidden;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #ccc;
position: relative; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-1 img {
width: 100%;
height: 100%;
border-radius: 50%; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-2 img {
width: 50%;
height: 100%;
position: absolute;
right: 0;
top: 0; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-2 img:first-child {
left: 0;
top: 0; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-3 img {
position: absolute;
width: 50%;
height: 50%;
right: 0;
top: 0; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-3 img:first-child {
left: 0;
top: 0;
width: 50%;
height: 100%; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-3 img:last-child {
bottom: 0;
right: 0;
top: 15px;
width: 50%;
height: 50%; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-4 img {
position: absolute;
width: 50%;
height: 50%;
right: 0;
top: 0; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:first-child {
left: 0;
top: 0;
width: 50%;
height: 100%; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:nth-child(3n) {
bottom: 0;
right: 0;
top: 15px;
width: 50%;
height: 50%; }
.chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:last-child {
left: 0;
bottom: 0;
top: 15px; }
.chanels .chanel .chanel-info {
flex-grow: 1;
padding-left: 8px;
padding-right: 8px;
overflow: hidden; }
.chanels .chanel .chanel-info h2 {
font-size: 13px;
font-weight: 400;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden; }
.chanels .chanel .chanel-info p {
font-size: 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden; }
.chanels .chanel.active {
background: rgba(0, 0, 0, 0.05); }
.chanels .chanel.notify .chanel-info p {
color: #2ecc71; }
.members .member {
display: flex;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
padding: 8px; }
.members .member .user-image {
width: 30px;
position: relative; }
.members .member .user-image img {
width: 30px;
height: 30px;
border-radius: 50%; }
.members .member .user-image .user-status {
width: 8px;
height: 8px;
display: block;
position: absolute;
right: 0;
bottom: 10px;
border: 1px solid #FFFFFF;
background: #cccccc;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%; }
.members .member .user-image .user-status.online {
background: #2ecc71; }
.members .member .member-info {
padding-left: 8px;
flex-grow: 1; }
.members .member .member-info h2 {
font-size: 14px; }
.members .member .member-info p {
font-size: 12px; }
h2.title {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.8); }
.toolbar {
height: 50px;
display: flex;
flex-direction: row;
position: relative; }
.toolbar span {
line-height: 20px;
height: 30px;
background: #2ecc71;
color: #FFF;
cursor: pointer;
display: block;
border-radius: 3px;
margin: 10px 5px 0 0;
padding: 5px 8px; }
.toolbar label {
line-height: 50px; }
.toolbar input {
height: 30px;
line-height: 30px;
margin-top: 10px;
border: 0 none; }
.toolbar .search-user {
min-width: 180px;
position: absolute;
left: 0;
top: 50px;
z-index: 1;
border: 1px solid rgba(0, 0, 0, 0.05);
border-top: 0 none; }
.toolbar .search-user .user-list {
display: flex;
flex-direction: column; }
.toolbar .search-user .user-list .user {
display: flex;
flex-direction: row;
padding: 5px;
border-bottom: 1px solid rgba(0, 0, 0, 0.05);
cursor: pointer; }
.toolbar .search-user .user-list .user img {
width: 30px;
height: 30px;
border-radius: 50%;
margin-top: 10px; }
.toolbar .search-user .user-list .user h2 {
padding-left: 8px;
flex-grow: 1;
font-size: 14px; }
.toolbar .search-user .user-list .user:last-child {
border-bottom: 0 none; }
.toolbar .search-user .user-list .user:hover {
background: rgba(0, 0, 0, 0.02); }
.user-bar {
position: relative; }
.user-bar button.login-btn {
height: 50px;
border: 0 none;
background: none;
color: #2ecc71;
font-weight: 600;
font-size: 14px; }
.user-bar .user-form {
background: #FFF;
box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
position: absolute;
top: 50px;
right: 0;
border: 1px solid rgba(0, 0, 0, 0.05);
border-top: 0 none;
padding: 10px; }
.user-bar .user-form .form-item label {
line-height: 30px;
min-width: 75px;
text-align: right;
margin-right: 8px; }
.user-bar .user-form .form-item input[type="email"], .user-bar .user-form .form-item input[type="password"], .user-bar .user-form .form-item input[type="text"] {
height: 30px;
line-height: 30px; }
.user-bar .user-form .form-actions {
display: flex;
flex-direction: row;
justify-content: flex-end; }
.user-bar .user-menu {
background: #FFF;
box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
min-width: 200px;
position: absolute;
right: 0;
top: 50px;
border: 1px solid rgba(0, 0, 0, 0.05);
border-top: 0 none; }
.user-bar .user-menu ul {
padding: 0;
margin: 0;
list-style: none; }
.user-bar .user-menu ul li {
border-top: 1px solid rgba(0, 0, 0, 0.05);
padding: 8px; }
.user-bar .user-menu ul li button {
background: none;
border: 0 none;
display: block;
cursor: pointer;
text-align: center;
width: 100%; }
.user-bar .user-menu ul li:hover {
background: rgba(0, 0, 0, 0.09); }
.user-bar .user-menu h2 {
font-size: 14px;
font-weight: 600;
margin: 0;
display: block;
text-align: center; }
.form-item {
display: flex;
margin-bottom: 10px; }
.form-item label {
font-weight: 600; }
.form-item input[type="email"], .form-item input[type="password"], .form-item input[type="text"] {
border: 1px solid rgba(0, 0, 0, 0.05);
padding: 3px 8px; }
.form-actions button {
border: 0 none;
padding: 7px 15px;
text-align: center; }
.form-actions button.primary {
background: #2ecc71;
color: #FFF; }
.app-message {
line-height: 1.5em;
padding: 10px;
font-size: 12px;
text-align: center;
border: 1px solid #2ecc71;
border-radius: 5px;
margin: 0 0 10px 0; }
.app-message.error {
background: #e74c3c;
color: #FFF;
border-color: #e74c3c; }
.user-status {
font-size: 10px;
color: #2c3e50; }
.user-status.online {
color: #2ecc71; }
.app-warning-state {
font-size: 10px;
padding: 0 10px;
color: #e74c3c; }
/*# sourceMappingURL=app.css.map */
================================================
FILE: app/src/css/app.scss
================================================
@import "font";
@import "https://fonts.googleapis.com/css?family=Open+Sans:400,600";
@import 'variable';
body, html {
margin: 0;
padding: 0;
height: 100%;
}
body {
color: $body-color;
font-size: 13px;
font-family: 'Open Sans', sans-serif;
}
* {
box-sizing: border-box;
padding: 0;
margin: 0;
}
.app-messenger {
display: flex;
flex-direction: column;
.header {
height: $header-height;
display: flex;
flex-direction: row;
border-bottom: 1px solid $border-color;
.left {
width: $left-sidebar-width;
position: relative;
.left-action {
position: absolute;
left: 8px;
top: 0;
}
.right-action {
position: absolute;
right: 8px;
top: 0;
}
h2 {
line-height: $header-height;
font-size: 14px;
font-weight: 600;
display: block;
text-align: center;
}
button {
background: none;
line-height: $header-height;
border: 0 none;
font-size: 20px;
cursor: pointer;
}
}
.content {
flex-grow: 1;
h2 {
line-height: $header-height;
text-align: center;
}
}
.right {
width: $right-sidebar-width;
.user-bar {
line-height: $header-height;
display: flex;
justify-content: flex-end;
padding: 0 10px;
.profile-name {
padding-right: 10px;
}
.profile-image {
line-height: $header-height;
img {
width: 30px;
height: 30px;
border-radius: 50%;
margin: 10px 0 0 0;
}
}
}
}
}
.main {
height: 100%;
display: flex;
overflow: hidden;
.sidebar-left {
width: $left-sidebar-width;
border-right: 1px solid $border-color;
}
.sidebar-right {
border-left: 1px solid $border-color;
width: $right-sidebar-width;
.title {
padding: 10px;
}
}
.content {
flex-grow: 1;
overflow: hidden;
display: flex;
flex-direction: column;
.messages {
flex-grow: 1;
}
.messenger-input {
border-top: 1px solid $border-color;
height: 50px;
display: flex;
flex-direction: row;
.text-input {
flex-grow: 1;
textarea {
border: 0 none;
width: 100%;
height: 100%;
padding: 8px 15px;
}
}
.actions {
button.send {
background: $primary-color;
color: #FFF;
border: 0 none;
padding: 7px 15px;
line-height: 50px;
}
}
}
}
}
}
.messages {
display: flex;
flex-direction: column;
overflow-y: auto;
height: 100%;
.message {
display: flex;
flex-direction: row;
justify-content: flex-start;
margin: 15px;
.message-user-image {
img {
width: 20px;
height: 20px;
border-radius: 50%;
}
}
.message-body {
padding-left: 10px;
.message-author {
}
.message-text {
background: rgba(0, 0, 0, 0.05);
padding: 10px;
border-radius: 10px;
}
}
&.me {
justify-content: flex-end;
.message-body {
.message-text {
background: $primary-color;
color: #FFF;
}
}
}
}
}
.chanels {
overflow-y: auto;
height: 100%;
.chanel {
cursor: pointer;
display: flex;
border-bottom: 1px solid $border-color;
padding: 8px;
.user-image {
width: 30px;
img {
max-width: 100%;
}
.channel-avatars {
overflow: hidden;
width: 30px;
height: 30px;
border-radius: 50%;
background-color: #ccc;
position: relative;
&.channel-avatars-1 {
img {
width: 100%;
height: 100%;
border-radius: 50%;
}
}
&.channel-avatars-2 {
img {
width: 50%;
height: 100%;
position: absolute;
right: 0;
top: 0;
&:first-child {
left: 0;
top: 0;
}
}
}
&.channel-avatars-3 {
img {
position: absolute;
width: 50%;
height: 50%;
right: 0;
top: 0;
&:first-child {
left: 0;
top: 0;
width: 50%;
height: 100%;
}
&:last-child {
bottom: 0;
right: 0;
top: 15px;
width: 50%;
height: 50%;
}
}
}
&.channel-avatars-4 {
img {
position: absolute;
width: 50%;
height: 50%;
right: 0;
top: 0;
&:first-child {
left: 0;
top: 0;
width: 50%;
height: 100%;
}
&:nth-child(3n) {
bottom: 0;
right: 0;
top: 15px;
width: 50%;
height: 50%;
}
&:last-child {
left: 0;
bottom: 0;
top: 15px;
}
}
}
}
}
.chanel-info {
flex-grow: 1;
padding-left: 8px;
padding-right: 8px;
overflow: hidden;
h2 {
font-size: 13px;
font-weight: 400;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
p {
font-size: 12px;
white-space: nowrap;
text-overflow: ellipsis;
overflow: hidden;
}
}
&.active {
background: rgba(0, 0, 0, 0.05);
}
&.notify {
.chanel-info {
p {
color: $primary-color;
}
}
}
}
}
.members {
.member {
display: flex;
border-bottom: 1px solid $border-color;
padding: 8px;
.user-image {
width: 30px;
position: relative;
img {
width: 30px;
height: 30px;
border-radius: 50%;
}
.user-status{
width: 8px;
height: 8px;
display: block;
position: absolute;
right: 0;
bottom: 10px;
border: 1px solid #FFFFFF;
background: #cccccc;
-webkit-border-radius: 50%;
-moz-border-radius: 50%;
border-radius: 50%;
&.online{
background: $primary-color;
}
}
}
.member-info {
padding-left: 8px;
flex-grow: 1;
h2 {
font-size: 14px;
}
p {
font-size: 12px;
}
}
}
}
h2.title {
font-size: 16px;
font-weight: 600;
color: rgba(0, 0, 0, 0.8);
}
.toolbar {
height: $header-height;
display: flex;
flex-direction: row;
position: relative;
span {
line-height: 20px;
height: 30px;
background: $primary-color;
color: #FFF;
cursor: pointer;
display: block;
border-radius: 3px;
margin: 10px 5px 0 0;
padding: 5px 8px;
}
label {
line-height: $header-height;
}
input {
height: 30px;
line-height: 30px;
margin-top: 10px;
border: 0 none;
}
.search-user {
min-width: 180px;
position: absolute;
left: 0;
top: $header-height;
z-index: 1;
border: 1px solid $border-color;
border-top: 0 none;
.user-list {
display: flex;
flex-direction: column;
.user {
display: flex;
flex-direction: row;
padding: 5px;
border-bottom: 1px solid $border-color;
cursor: pointer;
img {
width: 30px;
height: 30px;
border-radius: 50%;
margin-top: 10px;
}
h2 {
padding-left: 8px;
flex-grow: 1;
font-size: 14px;
}
&:last-child {
border-bottom: 0 none;
}
&:hover {
background: rgba(0, 0, 0, 0.02);
}
}
}
}
}
.user-bar {
position: relative;
button.login-btn {
height: $header-height;
border: 0 none;
background: none;
color: $primary-color;
font-weight: 600;
font-size: 14px;
}
.user-form {
background: #FFF;
box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
position: absolute;
top: $header-height;
right: 0;
border: 1px solid $border-color;
border-top: 0 none;
padding: 10px;
.form-item {
label {
line-height: 30px;
min-width: 75px;
text-align: right;
margin-right: 8px;
}
input[type="email"], input[type="password"], input[type="text"] {
height: 30px;
line-height: 30px;
}
}
.form-actions {
display: flex;
flex-direction: row;
justify-content: flex-end;
}
}
.user-menu {
background: #FFF;
box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);
min-width: 200px;
position: absolute;
right: 0;
top: $header-height;
border: 1px solid $border-color;
border-top: 0 none;
ul {
padding: 0;
margin: 0;
list-style: none;
li {
border-top: 1px solid $border-color;
padding: 8px;
button {
background: none;
border: 0 none;
display: block;
cursor: pointer;
text-align: center;
width: 100%;
}
&:hover {
background: rgba(0, 0, 0, 0.09);
}
}
}
h2 {
font-size: 14px;
font-weight: 600;
margin: 0;
display: block;
text-align: center;
}
}
}
.form-item {
display: flex;
margin-bottom: 10px;
label {
font-weight: 600;
}
input[type="email"], input[type="password"], input[type="text"] {
border: 1px solid $border-color;
padding: 3px 8px;
}
}
.form-actions {
button {
border: 0 none;
padding: 7px 15px;
text-align: center;
&.primary {
background: $primary-color;
color: #FFF;
}
}
}
.app-message {
line-height: 1.5em;
padding: 10px;
font-size: 12px;
text-align: center;
border: 1px solid $primary-color;
border-radius: 5px;
margin: 0 0 10px 0;
&.error {
background: $danger-color;
color: #FFF;
border-color: $danger-color;
}
}
.user-status{
font-size: 10px;
color: $body-color;
&.online{
color: $primary-color;
}
}
.app-warning-state{
font-size: 10px;
padding: 0 10px;
color: $danger-color;
}
================================================
FILE: app/src/helpers/index.js
================================================
================================================
FILE: app/src/helpers/objectid.js
================================================
/**
* Machine id.
*
* Create a random 3-byte value (i.e. unique for this
* process). Other drivers use a md5 of the machine id here, but
* that would mean an asyc call to gethostname, so we don't bother.
* @ignore
*/
var MACHINE_ID = parseInt(Math.random() * 0xffffff, 10);
// Regular expression that checks for hex value
var checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$');
// Check if buffer exists
try {
if (Buffer && Buffer.from) var hasBufferType = true;
} catch (err) {
hasBufferType = false;
}
/**
* Create a new ObjectID instance
*
* @class
* @param {(string|number)} id Can be a 24 byte hex string, 12 byte binary string or a Number.
* @property {number} generationTime The generation time of this ObjectId instance
* @return {ObjectID} instance of ObjectID.
*/
var ObjectID = function ObjectID(id) {
// Duck-typing to support ObjectId from different npm packages
if (id instanceof ObjectID) return id;
if (!(this instanceof ObjectID)) return new ObjectID(id);
this._bsontype = 'ObjectID';
// The most common usecase (blank id, new objectId instance)
if (id == null || typeof id === 'number') {
// Generate a new id
this.id = this.generate(id);
// If we are caching the hex string
if (ObjectID.cacheHexString) this.__id = this.toString('hex');
// Return the object
return;
}
// Check if the passed in id is valid
var valid = ObjectID.isValid(id);
// Throw an error if it's not a valid setup
if (!valid && id != null) {
throw new Error(
'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
);
} else if (valid && typeof id === 'string' && id.length === 24 && hasBufferType) {
return new ObjectID(new Buffer(id, 'hex'));
} else if (valid && typeof id === 'string' && id.length === 24) {
return ObjectID.createFromHexString(id);
} else if (id != null && id.length === 12) {
// assume 12 byte string
this.id = id;
} else if (id != null && id.toHexString) {
// Duck-typing to support ObjectId from different npm packages
return id;
} else {
throw new Error(
'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
);
}
if (ObjectID.cacheHexString) this.__id = this.toString('hex');
};
// Allow usage of ObjectId as well as ObjectID
// var ObjectId = ObjectID;
// Precomputed hex table enables speedy hex string conversion
var hexTable = [];
for (var i = 0; i < 256; i++) {
hexTable[i] = (i <= 15 ? '0' : '') + i.toString(16);
}
/**
* Return the ObjectID id as a 24 byte hex string representation
*
* @method
* @return {string} return the 24 byte hex string representation.
*/
ObjectID.prototype.toHexString = function() {
if (ObjectID.cacheHexString && this.__id) return this.__id;
var hexString = '';
if (!this.id || !this.id.length) {
throw new Error(
'invalid ObjectId, ObjectId.id must be either a string or a Buffer, but is [' +
JSON.stringify(this.id) +
']'
);
}
if (this.id instanceof _Buffer) {
hexString = convertToHex(this.id);
if (ObjectID.cacheHexString) this.__id = hexString;
return hexString;
}
for (var i = 0; i < this.id.length; i++) {
hexString += hexTable[this.id.charCodeAt(i)];
}
if (ObjectID.cacheHexString) this.__id = hexString;
return hexString;
};
/**
* Update the ObjectID index used in generating new ObjectID's on the driver
*
* @method
* @return {number} returns next index value.
* @ignore
*/
ObjectID.prototype.get_inc = function() {
return (ObjectID.index = (ObjectID.index + 1) % 0xffffff);
};
/**
* Update the ObjectID index used in generating new ObjectID's on the driver
*
* @method
* @return {number} returns next index value.
* @ignore
*/
ObjectID.prototype.getInc = function() {
return this.get_inc();
};
/**
* Generate a 12 byte id buffer used in ObjectID's
*
* @method
* @param {number} [time] optional parameter allowing to pass in a second based timestamp.
* @return {Buffer} return the 12 byte id buffer string.
*/
ObjectID.prototype.generate = function(time) {
if ('number' !== typeof time) {
time = ~~(Date.now() / 1000);
}
// Use pid
var pid =
(typeof process === 'undefined' || process.pid === 1
? Math.floor(Math.random() * 100000)
: process.pid) % 0xffff;
var inc = this.get_inc();
// Buffer used
var buffer = new Buffer(12);
// Encode time
buffer[3] = time & 0xff;
buffer[2] = (time >> 8) & 0xff;
buffer[1] = (time >> 16) & 0xff;
buffer[0] = (time >> 24) & 0xff;
// Encode machine
buffer[6] = MACHINE_ID & 0xff;
buffer[5] = (MACHINE_ID >> 8) & 0xff;
buffer[4] = (MACHINE_ID >> 16) & 0xff;
// Encode pid
buffer[8] = pid & 0xff;
buffer[7] = (pid >> 8) & 0xff;
// Encode index
buffer[11] = inc & 0xff;
buffer[10] = (inc >> 8) & 0xff;
buffer[9] = (inc >> 16) & 0xff;
// Return the buffer
return buffer;
};
/**
* Converts the id into a 24 byte hex string for printing
*
* @param {String} format The Buffer toString format parameter.
* @return {String} return the 24 byte hex string representation.
* @ignore
*/
ObjectID.prototype.toString = function(format) {
// Is the id a buffer then use the buffer toString method to return the format
if (this.id && this.id.copy) {
return this.id.toString(typeof format === 'string' ? format : 'hex');
}
// if(this.buffer )
return this.toHexString();
};
/**
* Converts to a string representation of this Id.
*
* @return {String} return the 24 byte hex string representation.
* @ignore
*/
ObjectID.prototype.inspect = ObjectID.prototype.toString;
/**
* Converts to its JSON representation.
*
* @return {String} return the 24 byte hex string representation.
* @ignore
*/
ObjectID.prototype.toJSON = function() {
return this.toHexString();
};
/**
* Compares the equality of this ObjectID with `otherID`.
*
* @method
* @param {object} otherID ObjectID instance to compare against.
* @return {boolean} the result of comparing two ObjectID's
*/
ObjectID.prototype.equals = function equals(otherId) {
// var id;
if (otherId instanceof ObjectID) {
return this.toString() === otherId.toString();
} else if (
typeof otherId === 'string' &&
ObjectID.isValid(otherId) &&
otherId.length === 12 &&
this.id instanceof _Buffer
) {
return otherId === this.id.toString('binary');
} else if (typeof otherId === 'string' && ObjectID.isValid(otherId) && otherId.length === 24) {
return otherId.toLowerCase() === this.toHexString();
} else if (typeof otherId === 'string' && ObjectID.isValid(otherId) && otherId.length === 12) {
return otherId === this.id;
} else if (otherId != null && (otherId instanceof ObjectID || otherId.toHexString)) {
return otherId.toHexString() === this.toHexString();
} else {
return false;
}
};
/**
* Returns the generation date (accurate up to the second) that this ID was generated.
*
* @method
* @return {date} the generation date
*/
ObjectID.prototype.getTimestamp = function() {
var timestamp = new Date();
var time = this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);
timestamp.setTime(Math.floor(time) * 1000);
return timestamp;
};
/**
* @ignore
*/
ObjectID.index = ~~(Math.random() * 0xffffff);
/**
* @ignore
*/
ObjectID.createPk = function createPk() {
return new ObjectID();
};
/**
* Creates an ObjectID from a second based number, with the rest of the ObjectID zeroed out. Used for comparisons or sorting the ObjectID.
*
* @method
* @param {number} time an integer number representing a number of seconds.
* @return {ObjectID} return the created ObjectID
*/
ObjectID.createFromTime = function createFromTime(time) {
var buffer = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);
// Encode time into first 4 bytes
buffer[3] = time & 0xff;
buffer[2] = (time >> 8) & 0xff;
buffer[1] = (time >> 16) & 0xff;
buffer[0] = (time >> 24) & 0xff;
// Return the new objectId
return new ObjectID(buffer);
};
// Lookup tables
//var encodeLookup = '0123456789abcdef'.split('');
var decodeLookup = [];
i = 0;
while (i < 10) decodeLookup[0x30 + i] = i++;
while (i < 16) decodeLookup[0x41 - 10 + i] = decodeLookup[0x61 - 10 + i] = i++;
var _Buffer = Buffer;
var convertToHex = function(bytes) {
return bytes.toString('hex');
};
/**
* Creates an ObjectID from a hex string representation of an ObjectID.
*
* @method
* @param {string} hexString create a ObjectID from a passed in 24 byte hexstring.
* @return {ObjectID} return the created ObjectID
*/
ObjectID.createFromHexString = function createFromHexString(string) {
// Throw an error if it's not a valid setup
if (typeof string === 'undefined' || (string != null && string.length !== 24)) {
throw new Error(
'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'
);
}
// Use Buffer.from method if available
if (hasBufferType) return new ObjectID(new Buffer(string, 'hex'));
// Calculate lengths
var array = new _Buffer(12);
var n = 0;
var i = 0;
while (i < 24) {
array[n++] = (decodeLookup[string.charCodeAt(i++)] << 4) | decodeLookup[string.charCodeAt(i++)];
}
return new ObjectID(array);
};
/**
* Checks if a value is a valid bson ObjectId
*
* @method
* @return {boolean} return true if the value is a valid bson ObjectId, return false otherwise.
*/
ObjectID.isValid = function isValid(id) {
if (id == null) return false;
if (typeof id === 'number') {
return true;
}
if (typeof id === 'string') {
return id.length === 12 || (id.length === 24 && checkForHexRegExp.test(id));
}
if (id instanceof ObjectID) {
return true;
}
if (id instanceof _Buffer) {
return true;
}
// Duck-Typing detection of ObjectId like objects
if (id.toHexString) {
return id.id.length === 12 || (id.id.length === 24 && checkForHexRegExp.test(id.id));
}
return false;
};
/**
* @ignore
*/
Object.defineProperty(ObjectID.prototype, 'generationTime', {
enumerable: true,
get: function() {
return this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);
},
set: function(value) {
// Encode time into first 4 bytes
this.id[3] = value & 0xff;
this.id[2] = (value >> 8) & 0xff;
this.id[1] = (value >> 16) & 0xff;
this.id[0] = (value >> 24) & 0xff;
}
});
/**
* Expose.
*/
module.exports = ObjectID;
module.exports.ObjectID = ObjectID;
module.exports.ObjectId = ObjectID;
================================================
FILE: app/src/index.js
================================================
import React from 'react';
import ReactDOM from 'react-dom';
import App from './components/app';
import './css/app.css'
//import registerServiceWorker from './registerServiceWorker';
ReactDOM.render(<App />, document.getElementById('root'));
//registerServiceWorker();
================================================
FILE: app/src/realtime.js
================================================
import _ from 'lodash'
import {OrderedMap} from 'immutable'
import {websocketUrl} from './config'
export default class Realtime {
constructor(store) {
this.store = store;
this.ws = null;
this.isConnected = false;
this.connect();
this.reconnect();
}
reconnect(){
const store = this.store;
window.setInterval(()=>{
const user = store.getCurrentUser();
if(user && !this.isConnected){
console.log("try reconnecting...");
this.connect();
}
}, 3000)
}
decodeMessage(msg) {
let message = {};
try {
message = JSON.parse(msg);
}
catch (err) {
console.log(err);
}
return message;
}
readMessage(msg) {
const store = this.store;
const currentUser = store.getCurrentUser();
const currentUserId = _.toString(_.get(currentUser, '_id'));
const message = this.decodeMessage(msg);
const action = _.get(message, 'action', '');
const payload = _.get(message, 'payload');
switch (action) {
case 'user_offline':
this.onUpdateUserStatus(payload, false);
break;
case 'user_online':
const isOnline = true;
this.onUpdateUserStatus(payload, isOnline);
break;
case 'message_added':
const activeChannel = store.getActiveChannel();
let notify = _.get(activeChannel, '_id') !== _.get(payload, 'channelId') && currentUserId !== _.get(payload, 'userId');
this.onAddMessage(payload, notify);
break;
case 'channel_added':
// to do check payload object and insert new channel to store.
this.onAddChannel(payload);
break;
default:
break;
}
}
onUpdateUserStatus(userId, isOnline = false){
const store = this.store;
store.users = store.users.update(userId, (user) => {
if(user){
user.online = isOnline;
}
return user;
});
store.update()
}
onAddMessage(payload, notify = false){
const store = this.store;
const currentUser = store.getCurrentUser();
const currentUserId = _.toString(_.get(currentUser, '_id'));
let user = _.get(payload, 'user');
// add user to cache
user = store.addUserToCache(user);
const messageObject = {
_id: payload._id,
body: _.get(payload, 'body', ''),
userId: _.get(payload, 'userId'),
channelId: _.get(payload, 'channelId'),
created: _.get(payload, 'created', new Date()),
me: currentUserId === _.toString(_.get(payload, 'userId')),
user: user,
};
store.setMessage(messageObject, notify);
}
onAddChannel(payload) {
const store = this.store;
const channelId = _.toString(_.get(payload, '_id'));
const userId = `${payload.userId}`;
const users = _.get(payload, 'users', []);
let channel = {
_id: channelId,
title: _.get(payload, 'title', ''),
isNew: false,
lastMessage: _.get(payload, 'lastMessage'),
members: new OrderedMap(),
messages: new OrderedMap(),
userId: userId,
created: new Date(),
};
_.each(users, (user) => {
// add this user to store.users collection
const memberId = `${user._id}`;
this.store.addUserToCache(user);
channel.members = channel.members.set(memberId, true);
});
const channelMessages = store.messages.filter((m) => _.toString(m.channelId)=== channelId);
channelMessages.forEach((msg) => {
const msgId = _.toString(_.get(msg, '_id'));
channel.messages = channel.messages.set(msgId, true);
})
store.addChannel(channelId, channel);
}
send(msg = {}) {
const isConnected = this.isConnected;
if (this.ws && isConnected) {
const msgString = JSON.stringify(msg);
this.ws.send(msgString);
}
}
authentication() {
const store = this.store;
const tokenId = store.getUserTokenId();
if (tokenId) {
const message = {
action: 'auth',
payload: `${tokenId}`
}
this.send(message);
}
}
connect() {
//console.log("Begin connecting to server via websocket.");
const ws = new WebSocket(websocketUrl);
this.ws = ws;
ws.onopen = () => {
//console.log("You are connected");
// let tell to the server who are you ?
this.isConnected = true;
this.authentication();
ws.onmessage = (event) => {
this.readMessage(_.get(event, 'data'));
console.log("Mesage from the server: ", event.data);
}
}
ws.onclose = () => {
//console.log("You disconnected!!!");
this.isConnected = false;
//this.store.update();
}
ws.onerror = () => {
this.isConnected = false;
this.store.update();
}
}
}
================================================
FILE: app/src/registerServiceWorker.js
================================================
// In production, we register a service worker to serve assets from local cache.
// This lets the app load faster on subsequent visits in production, and gives
// it offline capabilities. However, it also means that developers (and users)
// will only see deployed updates on the "N+1" visit to a page, since previously
// cached resources are updated in the background.
// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.
// This link also includes instructions on opting out of this behavior.
const isLocalhost = Boolean(
window.location.hostname === 'localhost' ||
// [::1] is the IPv6 localhost address.
window.location.hostname === '[::1]' ||
// 127.0.0.1/8 is considered localhost for IPv4.
window.location.hostname.match(
/^127(?:\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/
)
);
export default function register() {
if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {
// The URL constructor is available in all browsers that support SW.
const publicUrl = new URL(process.env.PUBLIC_URL, window.location);
if (publicUrl.origin !== window.location.origin) {
// Our service worker won't work if PUBLIC_URL is on a different origin
// from what our page is served on. This might happen if a CDN is used to
// serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374
return;
}
window.addEventListener('load', () => {
const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;
if (isLocalhost) {
// This is running on localhost. Lets check if a service worker still exists or not.
checkValidServiceWorker(swUrl);
} else {
// Is not local host. Just register service worker
registerValidSW(swUrl);
}
});
}
}
function registerValidSW(swUrl) {
navigator.serviceWorker
.register(swUrl)
.then(registration => {
registration.onupdatefound = () => {
const installingWorker = registration.installing;
installingWorker.onstatechange = () => {
if (installingWorker.state === 'installed') {
if (navigator.serviceWorker.controller) {
// At this point, the old content will have been purged and
// the fresh content will have been added to the cache.
// It's the perfect time to display a "New content is
// available; please refresh." message in your web app.
console.log('New content is available; please refresh.');
} else {
// At this point, everything has been precached.
// It's the perfect time to display a
// "Content is cached for offline use." message.
console.log('Content is cached for offline use.');
}
}
};
};
})
.catch(error => {
console.error('Error during service worker registration:', error);
});
}
function checkValidServiceWorker(swUrl) {
// Check if the service worker can be found. If it can't reload the page.
fetch(swUrl)
.then(response => {
// Ensure service worker exists, and that we really are getting a JS file.
if (
response.status === 404 ||
response.headers.get('content-type').indexOf('javascript') === -1
) {
// No service worker found. Probably a different app. Reload the page.
navigator.serviceWorker.ready.then(registration => {
registration.unregister().then(() => {
window.location.reload();
});
});
} else {
// Service worker found. Proceed as normal.
registerValidSW(swUrl);
}
})
.catch(() => {
console.log(
'No internet connection found. App is running in offline mode.'
);
});
}
export function unregister() {
if ('serviceWorker' in navigator) {
navigator.serviceWorker.ready.then(registration => {
registration.unregister();
});
}
}
================================================
FILE: app/src/service.js
================================================
import axios from 'axios'
import {apiUrl} from './config'
const apiURL = apiUrl;
export default class Service{
get(endpoint, options = null){
const url = `${apiURL}/${endpoint}`;
return axios.get(url, options);
}
post(endpoint = "", data = {}, options = {headers: {'Content-Type': 'application/json'}}){
const url = `${apiURL}/${endpoint}`;
return axios.post(url, data, options);
}
}
================================================
FILE: app/src/store.js
================================================
import {OrderedMap} from 'immutable'
import _ from 'lodash'
import Service from './service'
import Realtime from './realtime'
export default class Store {
constructor(appComponent) {
this.app = appComponent;
this.service = new Service();
this.messages = new OrderedMap();
this.channels = new OrderedMap();
this.activeChannelId = null;
this.token = this.getTokenFromLocalStore();
this.user = this.getUserFromLocalStorage();
this.users = new OrderedMap();
this.search = {
users: new OrderedMap(),
}
this.realtime = new Realtime(this);
this.fetchUserChannels();
}
isConnected(){
return this.realtime.isConnected;
}
fetchUserChannels(){
const userToken = this.getUserTokenId();
if(userToken){
const options = {
headers: {
authorization: userToken,
}
}
this.service.get(`api/me/channels`, options).then((response) => {
const channels = response.data;
_.each(channels, (c) => {
this.realtime.onAddChannel(c);
});
const firstChannelId = _.get(channels, '[0]._id', null);
this.fetchChannelMessages(firstChannelId);
}).catch((err) => {
console.log("An error fetching user channels", err);
})
}
}
addUserToCache(user) {
user.avatar = this.loadUserAvatar(user);
const id = _.toString(user._id);
this.users = this.users.set(id, user);
return user;
}
getUserTokenId() {
return _.get(this.token, '_id', null);
}
loadUserAvatar(user) {
return `https://api.adorable.io/avatars/100/${user._id}.png`
}
startSearchUsers(q = "") {
// query to backend servr and get list of users.
const data = {search: q};
this.search.users = this.search.users.clear();
this.service.post('api/users/search', data).then((response) => {
// list of users matched.
const users = _.get(response, 'data', []);
_.each(users, (user) => {
// cache to this.users
// add user to this.search.users
user.avatar = this.loadUserAvatar(user);
const userId = `${user._id}`;
this.users = this.users.set(userId, user);
this.search.users = this.search.users.set(userId, user);
});
// update component
this.update();
}).catch((err) => {
//console.log("searching errror", err);
})
}
setUserToken(accessToken) {
if (!accessToken) {
this.localStorage.removeItem('token');
this.token = null;
return;
}
this.token = accessToken;
localStorage.setItem('token', JSON.stringify(accessToken));
}
getTokenFromLocalStore() {
if (this.token) {
return this.token;
}
let token = null;
const data = localStorage.getItem('token');
if (data) {
try {
token = JSON.parse(data);
}
catch (err) {
console.log(err);
}
}
return token;
}
getUserFromLocalStorage() {
let user = null;
const data = localStorage.getItem('me');
try {
user = JSON.parse(data);
}
catch (err) {
console.log(err);
}
if (user) {
// try to connect to backend server and verify this user is exist.
const token = this.getTokenFromLocalStore();
const tokenId = _.get(token, '_id');
const options = {
headers: {
authorization: tokenId,
}
}
this.service.get('api/users/me', options).then((response) => {
// this mean user is logged with this token id.
const accessToken = response.data;
const user = _.get(accessToken, 'user');
this.setCurrentUser(user);
this.setUserToken(accessToken);
}).catch(err => {
this.signOut();
});
}
return user;
}
setCurrentUser(user) {
// set temporary user avatar image url
user.avatar = this.loadUserAvatar(user);
this.user = user;
if (user) {
localStorage.setItem('me', JSON.stringify(user));
// save this user to our users collections in local
const userId = `${user._id}`;
this.users = this.users.set(userId, user);
}
this.update();
}
clearCacheData(){
this.channels = this.channels.clear();
this.messages = this.messages.clear();
this.users = this.users.clear();
}
signOut() {
const userId = _.toString(_.get(this.user, '_id', null));
const tokenId = _.get(this.token, '_id', null); //this.token._id;
// request to backend and loggout this user
const options = {
headers: {
authorization: tokenId,
}
};
this.service.get('api/me/logout', options);
this.user = null;
localStorage.removeItem('me');
localStorage.removeItem('token');
this.clearCacheData();
if (userId) {
this.users = this.users.remove(userId);
}
this.update();
}
register(user){
return new Promise((resolve, reject) => {
this.service.post('api/users', user).then((response) => {
console.log("use created", response.data);
return resolve(response.data);
}).catch(err => {
return reject("An error create your account");
})
});
}
login(email = null, password = null) {
const userEmail = _.toLower(email);
const user = {
email: userEmail,
password: password,
}
//console.log("Ttrying to login with user info", user);
return new Promise((resolve, reject) => {
// we call to backend service and login with user data
this.service.post('api/users/login', user).then((response) => {
// that mean successful user logged in
const accessToken = _.get(response, 'data');
const user = _.get(accessToken, 'user');
this.setCurrentUser(user);
this.setUserToken(accessToken);
// call to realtime and connect again to socket server with this user
this.realtime.connect();
// begin fetching user's channels
this.fetchUserChannels();
//console.log("Got user login callback from the server: ", accessToken);
}).catch((err) => {
console.log("Got an error login from server", err);
// login error
const message = _.get(err, 'response.data.error.message', "Login Error!");
return reject(message);
})
});
}
removeMemberFromChannel(channel = null, user = null) {
if (!channel || !user) {
return;
}
const userId = _.get(user, '_id');
const channelId = _.get(channel, '_id');
channel.members = channel.members.remove(userId);
this.channels = this.channels.set(channelId, channel);
this.update();
}
addUserToChannel(channelId, userId) {
const channel = this.channels.get(channelId);
if (channel) {
// now add this member id to channels members.
channel.members = channel.members.set(userId, true);
this.channels = this.channels.set(channelId, channel);
this.update();
}
}
getSearchUsers() {
return this.search.users.valueSeq();
}
onCreateNewChannel(channel = {}) {
const channelId = _.get(channel, '_id');
this.addChannel(channelId, channel);
this.setActiveChannelId(channelId);
//console.log(JSON.stringify(this.channels.toJS()));
}
getCurrentUser() {
return this.user;
}
fetchChannelMessages(channelId){
let channel = this.channels.get(channelId);
if (channel && !_.get(channel, 'isFetchedMessages')){
const token = _.get(this.token, '_id');//this.token._id;
const options = {
headers: {
authorization: token,
}
}
this.service.get(`api/channels/${channelId}/messages`, options).then((response) => {
channel.isFetchedMessages = true;
const messages = response.data;
_.each(messages, (message) => {
this.realtime.onAddMessage(message);
});
this.channels = this.channels.set(channelId, channel);
}).catch((err) => {
console.log("An error fetching channel 's messages", err);
})
}
}
setActiveChannelId(id) {
this.activeChannelId = id;
this.fetchChannelMessages(id);
this.update();
}
getActiveChannel() {
const channel = this.activeChannelId ? this.channels.get(this.activeChannelId) : this.channels.first();
return channel;
}
setMessage(message, notify = false) {
const id = _.toString(_.get(message, '_id'));
this.messages = this.messages.set(id, message);
const channelId = _.toString(message.channelId);
const channel = this.channels.get(channelId);
if (channel) {
channel.messages = channel.messages.set(id, true);
channel.lastMessage = _.get(message, 'body', '');
channel.notify = notify;
this.channels = this.channels.set(channelId, channel);
} else {
// fetch to the server with channel info
this.service.get(`api/channels/${channelId}`).then((response) => {
const channel = _.get(response, 'data');
/*const users = _.get(channel, 'users');
_.each(users, (user) => {
this.addUserToCache(user);
});*/
this.realtime.onAddChannel(channel);
})
}
this.update();
}
addMessage(id, message = {}) {
// we need add user object who is author of this message
const user = this.getCurrentUser();
message.user = user;
this.messages = this.messages.set(id, message);
// let's add new message id to current channel->messages.
const channelId = _.get(message, 'channelId');
if (channelId) {
let channel = this.channels.get(channelId);
channel.lastMessage = _.get(message, 'body', '');
// now send this channel info to the server
const obj = {
action: 'create_channel',
payload: channel,
};
this.realtime.send(obj);
//console.log("channel:", channel);
// send to the server via websocket to creawte new message and notify to other members.
this.realtime.send(
{
action: 'create_message',
payload: message,
}
);
channel.messages = channel.messages.set(id, true);
channel.isNew = false;
this.channels = this.channels.set(channelId, channel);
}
this.update();
// console.log(JSON.stringify(this.messages.toJS()));
}
getMessages() {
return this.messages.valueSeq();
}
getMessagesFromChannel(channel) {
let messages = new OrderedMap();
if (channel) {
channel.messages.forEach((value, key) => {
const message = this.messages.get(key);
messages = messages.set(key, message);
});
}
return messages.valueSeq();
}
getMembersFromChannel(channel) {
let members = new OrderedMap();
if (channel) {
channel.members.forEach((value, key) => {
const userId = `${key}`;
const user = this.users.get(userId);
const loggedUser = this.getCurrentUser();
if (_.get(loggedUser, '_id') !== _.get(user, '_id')) {
members = members.set(key, user);
}
});
}
return members.valueSeq();
}
addChannel(index, channel = {}) {
this.channels = this.channels.set(`${index}`, channel);
this.update();
}
getChannels() {
//return this.channels.valueSeq();
// we need to sort channel by date , the last one will list on top.
this.channels = this.channels.sort((a, b) => a.updated < b.updated);
return this.channels.valueSeq();
}
update() {
this.app.forceUpdate()
}
}
================================================
FILE: deployment-to-digitalocean-hosting.md
================================================
# Deploy Reactjs, Nodejs Chat app to DigitalOcean hosting (Ubuntu VPS)
## Get DigitalOcean account
I have been using <a href="https://m.do.co/c/bb792e37b9dd">DigitalOcean</a> for me and setup for my customers, so I recommend to use it for your project as well. Just pick the VPS best suited for the size of your project, starting at 5$, 10$ or 20$. The price is very flexible. DO provides SSD cloud hosting at a good price - I don't think you can get same price on other providers with same quality.
Besides, their support is very fast and they have good documentation, and a friendly UI for end-users.
So lets get started by registering an account and deploy your app at <a href="https://m.do.co/c/bb792e37b9dd">Digitalocean.com</a>
## Setup Ubuntu on DigitalOcean Cloud VPS.
* In this tutorial I will use Ubuntu 16.04, and I also recommend this OS for your VPS. I recommend you choose a suitable VPN, depending on how much traffic you expect. I will start at 20$ a month, and upgrade if necessary.
* Choose a data center region: DigitalOcean has many data centers, which means you can pick the data center near where you expect most of your visitors to live. For example, if I expect all the visitors to come from Vietnam, I will choose the data center closest to Vietnam, which is Singapore.
* Select additional options if you want to have additional backup service, or private network.
* Add your SSH keys: you can generate your SSH key on your computer and copy it to your VPS, which means when you login from SSH, you're not required to enter a username and password. This is more secure and will save you time. If you would like to know how to generate SSH key and use it on DigitalOcean hosting, I recommend this <a href="https://www.digitalocean.com/community/tutorials/how-to-use-SSH-keys-with-digitalocean-droplets">article</a>
* By default, you create one droplet at a time. If you want to, you can set up multiple droplets at once.
* Name your droplet and click submit, just get a cup of coffee and wait a moment for DigitalOcean to set everything up for you. When you see "Happy coding!" your cloud VPS is ready for use.
* Check your email that you registered with on DigitalOcean. You should receive an email notifying you about your VPS IP, root username and password.
This is the format of the email.
Droplet Name: [Name of your Droplet]
IP Address: [your-VPS-IP]
Username: root
Password: [your-root-password-generated-by-robot]
* Login to your Cloud via terminal by writing
```
ssh root@YOUR-IP-ADDREESS
```
Now, enter the root password given in the email. You will be asked for a new password the first time logging in.
+ The server will ask you for your password once more (the password given in the email).
+ Enter a new password
+ Confirm the password, and remember it for later
+ The setup is complete.
## Configuring the Firewall on your Cloud
This is a very important step we need to do. We need to reconfigure our firewall software to allow access to the service
* I recommend open port only for 80, 443, SSH (port 22), but it depends on your project, and it may need more ports open for any other services. In this project we will open port 80 for http access, 443 https (ssl), and port 22 (for SSH login). This will suffice.
* By default Firewall is inactive, which you can check by running
``` sudo ufw status ```
```
sudo ufw app list
```
* So let us config the Firewall and allow those ports by
```
sudo ufw allow 'Nginx Full'
```
```
sudo ufw allow 'OpenSSH'
```
```
sudo ufw enable
```
## Setup Nodejs on DigitalOcean Ubuntu 16.04
We are using Nodejs for backend and we will serve the static files of the react application build. So Nodejs is required
* Visit https://nodejs.org/en/download/package-manager/ to see the documentation
* We use package management to install, here is command to install Node.js v9
```
curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -
```
```
sudo apt-get install -y nodejs
```
* After successfully installing Node.js, we can check the version by typing in the command ``` node -v ``` and you should see the current version (v9.3.0 at the time of this writing).
## Setup MongoDB v3.6 on DigitalOcean Ubuntu 16.04 Cloud VPS
We are using MongoDB as a database, and so we can install it my following the documentation https://docs.mongodb.com/manual/tutorial/install-mongodb-on-ubuntu/
* Import the public key used by the package management system
```
sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5
```
* Create a list file for MongoDB (Ubuntu 16.04)
```
echo "deb [ arch=amd64,arm64 ] https://repo.mongodb.org/apt/ubuntu xenial/mongodb-org/3.6 multiverse" | sudo tee /etc/apt/sources.list.d/mongodb-org-3.6.list
```
* Reload local package database
```
sudo apt-get update
```
* Install the latest stable version of MongoDB
```
sudo apt-get install -y mongodb-org
```
* Start MongoDB by running (default port: 27017)
```
sudo service mongod start
```
* Stop MongoDB by running
```
sudo service mongod stop
```
* Restart MongoDB by running
```
sudo service mongod restart
```
## Install Nginx - Http Proxy Server
Let me explain simply why we use Nginx for this Nodejs web application.
When we run our chat app, it will run on port 3000, which is the default for running a Nodejs application. We can change the port to 3001, 3002 or 8080, and so on... However, if you point your domain to DigitalOcean cloud VPS, you can access your app throught the domain. For example, you can reach a Nodejs app on the VPS with a port 3000 by vising https://tabvn.com:3000.
In order to set a nodejs web app on the default port of 80, which can be visited by simply going to http://tabvn.com/, we use Nginx.
* To install Nginx, visit the official documentation at http://nginx.org/en/linux_packages.html
* So we will run following command on Ubuntu cloud VPS 16.04
```
apt-get update
```
```
sudo apt-get install nginx
```
* Start Nginx: open your IP-address, for example: http://123.456.789. You should see "Welcome to nginx!". All the Nginx configurations is in our cloud at the location /etc/nginx/nginx.conf
```
nginx
```
* Stop Nginx
```
nginx -s stop
```
* Reload Nginx
```
nginx -s reload
```
* Close your Cloud command line by ``` exit ``` or cloud command line tab in terminal
## Time to Deployment
* Download the chat app project at https://github.com/tabvn/nodejs-reactjs-chatapp.
```
git clone https://github.com/tabvn/nodejs-reactjs-chatapp.git chatApp
```
```
cd chatApp
```
```
cd server
```
```
npm install
```
```
cd ../app
```
```
npm install
```
* Fixed issue of bcrypt on Ubuntu 16.04
```
sudo apt-get install build-essential
```
## Nginx config sample:
Nginx Websocket document: http://nginx.org/en/docs/http/websocket.html
```
server {
listen 80;
root /var/www/html;
location / {
proxy_pass http://127.0.0.1:3001;
proxy_http_version 1.1;
proxy_set_header Upgrade $http_upgrade;
proxy_set_header Connection "upgrade";
}
}
```
See Video: https://www.youtube.com/watch?v=wJsH45eWNBo
================================================
FILE: server/Dockerfile
================================================
FROM ubuntu:18.04
#RUN apk add --update \ libc6-compat
# Install a bunch of node modules that are commonly used.
#ADD package.json /usr/app/
RUN apt-get update && apt-get -qq -y install curl
ENV NODE_VERSION=9.9.0
RUN apt-get install -y curl
RUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash
ENV NVM_DIR=/root/.nvm
RUN . "$NVM_DIR/nvm.sh" && nvm install ${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm use v${NODE_VERSION}
RUN . "$NVM_DIR/nvm.sh" && nvm alias default v${NODE_VERSION}
ENV PATH="/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}"
RUN node --version
RUN npm --version
ADD . /usr/app/
WORKDIR /usr/app
#RUN npm install -g n
#RUN n 8.4.0
#RUN npm install uNetworking/uWebSockets.js#v15.11.0
RUN npm install
#RUN npm rebuild uws
================================================
FILE: server/docker-compose.yml
================================================
version: '3'
services:
server:
build: .
ports:
- "3001:3001"
depends_on:
- mongo
command: npm run dev
mongo:
image: mongo
ports:
- "27017:27017"
================================================
FILE: server/package.json
================================================
{
"name": "chatapp",
"version": "1.0.0",
"description": "Use websocket in application.",
"main": "index.js",
"scripts": {
"dev": "nodemon -w src --exec \"babel-node src --presets env,stage-0\"",
"build": "babel src -s -D -d dist --presets env,stage-0",
"start": "node dist",
"prestart": "npm run -s build",
"test": "eslint src"
},
"eslintConfig": {
"extends": "eslint:recommended",
"parserOptions": {
"ecmaVersion": 7,
"sourceType": "module"
},
"env": {
"node": true
},
"rules": {
"no-console": 0,
"no-unused-vars": 1
}
},
"dependencies": {
"bcrypt": "^2.0.0",
"body-parser": "^1.18.2",
"cors": "^2.8.4",
"express": "^4.16.2",
"immutable": "^3.8.2",
"lodash": "^4.17.4",
"moment": "^2.19.3",
"mongodb": "^2.2.33",
"morgan": "^1.9.0",
"uws": "^9.14.0"
},
"devDependencies": {
"babel-cli": "^6.26.0",
"babel-core": "^6.26.0",
"babel-preset-env": "^1.6.1",
"babel-preset-stage-0": "^6.24.1",
"eslint": "^4.9.0",
"nodemon": "^1.12.1"
},
"author": "toan@tabvn.com",
"license": "ISC"
}
================================================
FILE: server/src/app-router.js
================================================
import moment from 'moment';
import _ from 'lodash'
export const START_TIME = new Date();
export default class AppRouter {
constructor(app) {
this.app = app;
this.setupRouter = this.setupRouter.bind(this);
this.setupRouter();
}
setupRouter() {
const app = this.app;
console.log("APp ROuter works!");
/**
* @endpoint: /api/users
* @method: POST
**/
app.post('/api/users', (req, res, next) => {
const body = req.body;
app.models.user.create(body).then((user) => {
_.unset(user, 'password');
return res.status(200).json(user);
}).catch(err => {
return res.status(503).json({error: err});
})
});
/**
* @endpoint: /api/users/me
* @method: GET
**/
app.get('/api/users/me', (req, res, next) => {
let tokenId = req.get('authorization');
if (!tokenId) {
// get token from query
tokenId = _.get(req, 'query.auth');
}
app.models.token.loadTokenAndUser(tokenId).then((token) => {
_.unset(token, 'user.password');
return res.json(token);
}).catch(err => {
return res.status(401).json({
error: err
})
});
});
/**
* @endpoint: /api/users/search
* @method: POST
**/
app.post('/api/users/search', (req, res, next) => {
const keyword = _.get(req, 'body.search', '');
app.models.user.search(keyword).then((results) => {
return res.status(200).json(results);
}).catch((err) => {
return res.status(404).json({
error: 'Not found.'
})
})
});
/**
* @endpoint: /api/users/:id
* @method: GET
**/
app.get('/api/users/:id', (req, res, next) => {
const userId = _.get(req, 'params.id');
app.models.user.load(userId).then((user) => {
_.unset(user, 'password');
return res.status(200).json(user);
}).catch(err => {
return res.status(404).json({
error: err,
})
})
});
/**
* @endpoint: /api/users/login
* @method: POST
**/
app.post('/api/users/login', (req, res, next) => {
const body = _.get(req, 'body');
app.models.user.login(body).then((token) => {
_.unset(token, 'user.password');
return res.status(200).json(token);
}).catch(err => {
return res.status(401).json({
error: err
})
})
})
/**
* @endpoint: /api/channels/:id
* @method: GET
**/
app.get('/api/channels/:id', (req, res, next) => {
const channelId = _.get(req, 'params.id');
console.log(channelId);
if (!channelId) {
return res.status(404).json({error: {message: "Not found."}});
}
app.models.channel.load(channelId).then((channel) => {
// fetch all uses belong to memberId
const members = channel.members;
const query = {
_id: {$in: members}
};
const options = {_id: 1, name: 1, created: 1};
app.models.user.find(query, options).then((users) => {
channel.users = users;
return res.status(200).json(channel);
}).catch(err => {
return res.status(404).json({error: {message: "Not found."}});
});
}).catch((err) => {
return res.status(404).json({error: {message: "Not found."}});
})
});
/**
* @endpoint: /api/channels/:id/messages
* @method: GET
**/
app.get('/api/channels/:id/messages', (req, res, next) => {
let tokenId = req.get('authorization');
if (!tokenId) {
// get token from query
tokenId = _.get(req, 'query.auth');
}
app.models.token.loadTokenAndUser(tokenId).then((token) => {
const userId = token.userId;
// make sure user are logged in
// check if this user is inside of channel members. other retun 401.
let filter = _.get(req, 'query.filter', null);
if (filter) {
filter = JSON.parse(filter);
console.log(filter);
}
const channelId = _.toString(_.get(req, 'params.id'));
const limit = _.get(filter, 'limit', 50);
const offset = _.get(filter, 'offset', 0);
// load channel
this.app.models.channel.load(channelId).then((c) => {
const memberIds = _.get(c, 'members');
const members = [];
_.each(memberIds, (id) => {
members.push(_.toString(id));
})
if (!_.includes(members, _.toString(userId))) {
return res.status(401).json({error: {message: "Access denied"}});
}
this.app.models.message.getChannelMessages(channelId, limit, offset).then((messages) => {
return res.status(200).json(messages);
}).catch((err) => {
return res.status(404).json({error: {message: "Not found."}});
})
}).catch((err) => {
return res.status(404).json({error: {message: "Not found."}});
})
}).catch((err) => {
return res.status(401).json({error: {message: "Access denied"}});
});
});
/**
* @endpoint: /api/me/channels
* @method: GET
**/
app.get('/api/me/channels', (req, res, next) => {
let tokenId = req.get('authorization');
if (!tokenId) {
// get token from query
tokenId = _.get(req, 'query.auth');
}
app.models.token.loadTokenAndUser(tokenId).then((token) => {
const userId = token.userId;
const query = [
{
$lookup: {
from: 'users',
localField: 'members',
foreignField: '_id',
as: 'users',
}
},
{
$match: {
members: {$all: [userId]}
}
},
{
$project: {
_id: true,
title: true,
lastMessage: true,
created: true,
updated: true,
userId: true,
users: {
_id: true,
name: true,
created: true,
online: true
},
members: true,
}
},
{
$sort: {updated: -1, created: -1}
},
{
$limit: 50,
}
];
app.models.channel.aggregate(query).then((channels) => {
return res.status(200).json(channels);
}).catch((err) => {
return res.status(404).json({error: {message: "Not found."}});
})
}).catch(err => {
return res.status(401).json({
error: "Access denied."
})
});
});
/**
* @endpoint: /api/me/logout
* @method: GET
**/
app.get('/api/me/logout', (req, res, next) => {
let tokenId = req.get('authorization');
if (!tokenId) {
// get token from query
tokenId = _.get(req, 'query.auth');
}
app.models.token.loadTokenAndUser(tokenId).then((token) => {
app.models.token.logout(token);
return res.status(200).json({
message: 'Successful.'
});
}).catch(err => {
return res.status(401).json({error: {message: 'Access denied'}});
})
})
}
}
================================================
FILE: server/src/database.js
================================================
import {MongoClient} from 'mongodb'
const URL = 'mongodb://mongo:27017';
export default class Database{
connect(){
return new Promise((resolve, reject) => {
MongoClient.connect(URL, (err, db) => {
return err ? reject(err) : resolve(db);
});
});
}
}
================================================
FILE: server/src/helper.js
================================================
export const isEmail = (emaill) => {
const regex = /^(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/
return regex.test(emaill);
}
export const toString = (id = "") => {
return `${id}`;
}
================================================
FILE: server/src/index.js
================================================
import http from 'http';
import express from 'express';
import cors from 'cors';
import bodyParser from 'body-parser';
import {version} from '../package.json'
import WebSocketServer, {Server} from 'uws';
import AppRouter from './app-router'
import Model from './models'
import Database from './database'
import path from 'path'
const PORT = 3001;
const app = express();
app.server = http.createServer(app);
//app.use(morgan('dev'));
app.use(cors({
exposedHeaders: "*"
}));
app.use(bodyParser.json({
limit: '50mb'
}));
app.wss = new Server({
server: app.server
});
// static www files use express
const wwwPath = path.join(__dirname, 'www');
app.use('/', express.static(wwwPath));
// Connect to Mongo Database
new Database().connect().then((db) => {
console.log("Successful connected to database.")
app.db = db;
}).catch((err) => {
throw(err);
});
// End connect to Mongodb Database
app.models = new Model(app);
app.routers = new AppRouter(app);
app.server.listen(process.env.PORT || PORT, () => {
console.log(`App is running on port ${app.server.address().port}`);
});
export default app;
================================================
FILE: server/src/models/channel.js
================================================
import _ from 'lodash'
import {toString} from '../helper'
import {ObjectID} from 'mongodb'
import {OrderedMap} from 'immutable'
export default class Channel {
constructor(app) {
this.app = app;
this.channels = new OrderedMap();
}
aggregate(q){
return new Promise((resolve, reject) => {
this.app.db.collection('channels').aggregate(q, (err, results) => {
return err ? reject(err) : resolve(results);
});
})
}
find(q, options = {}){
return new Promise((resolve, reject) => {
this.app.db.collection('channels').find(q, options).toArray((err, results) => {
return err ? reject(err) : resolve(results);
});
});
}
load(id) {
return new Promise((resolve, reject) => {
id = _.toString(id);
// first find in cache
const channelFromCache = this.channels.get(id);
if (channelFromCache) {
return resolve(channelFromCache);
}
// let find in db
this.findById(id).then((c) => {
this.channels = this.channels.set(id, c);
return resolve(c);
}).catch((err) => {
return reject(err);
})
})
}
findById(id){
return new Promise((resolve, reject) => {
this.app.db.collection('channels').findOne({_id: new ObjectID(id)}, (err, result) => {
if(err || !result){
return reject(err ? err : "Not found");
}
return resolve(result);
});
})
}
create(obj) {
return new Promise((resolve, reject) => {
let id = toString(_.get(obj, '_id'));
let idObject = id ? new ObjectID(id) : new ObjectID();
let members = [];
_.each(_.get(obj, 'members', []), (value, key) => {
const memberObjectId = new ObjectID(key);
members.push(memberObjectId);
});
let userIdObject = null;
let userId = _.get(obj, 'userId', null);
if (userId) {
userIdObject = new ObjectID(userId);
}
const channel = {
_id: idObject,
title: _.get(obj, 'title', ''),
lastMessage: _.get(obj, 'lastMessage', ''),
created: new Date(),
userId: userIdObject,
members: members,
}
this.app.db.collection('channels').insertOne(channel, (err, info) => {
if (!err) {
const channelId = channel._id.toString();
this.channels = this.channels.set(channelId, channel);
}
return err ? reject(err) : resolve(channel);
});
});
}
}
================================================
FILE: server/src/models/connection.js
================================================
import {OrderedMap} from 'immutable'
import {ObjectID} from 'mongodb'
import _ from 'lodash'
export default class Connection {
constructor(app) {
this.app = app;
this.connections = OrderedMap();
this.modelDidLoad();
}
decodeMesasge(msg) {
let messageObject = null;
try {
messageObject = JSON.parse(msg);
}
catch (err) {
console.log("An error decode the socket mesage", msg);
}
return messageObject;
}
sendToMembers(userId, obj) {
const query = [
{
$match: {
members: {$all: [new ObjectID(userId)]}
}
},
{
$lookup: {
from: 'users',
localField: 'members',
foreignField: '_id',
as: 'users'
}
},
{
$unwind: {
path: '$users'
}
},
{
$match: {'users.online': {$eq: true}}
},
{
$group: {
_id: "$users._id"
}
}
];
const users = [];
this.app.db.collection('channels').aggregate(query, (err, results) => {
// console.log("found members array who is chattting with current user", results);
if (err === null && results) {
_.each(results, (result) => {
const uid = _.toString(_.get(result, '_id'));
if (uid) {
users.push(uid);
}
});
// this is list of all connections is chatting with current user
const memberConnections = this.connections.filter((con) => _.includes(users, _.toString(_.get(con, 'userId'))));
if (memberConnections.size) {
memberConnections.forEach((connection, key) => {
const ws = connection.ws;
this.send(ws, obj);
});
}
}
})
}
sendAll(obj) {
// send socket messages to all clients.
this.connections.forEach((con, key) => {
const ws = con.ws;
this.send(ws, obj);
});
}
send(ws, obj) {
const message = JSON.stringify(obj);
ws.send(message);
}
doTheJob(socketId, msg) {
const action = _.get(msg, 'action');
const payload = _.get(msg, 'payload');
const userConnection = this.connections.get(socketId);
switch (action) {
case 'create_message':
if (userConnection.isAuthenticated) {
let messageObject = payload;
messageObject.userId = _.get(userConnection, 'userId');
//console.log("Got message from client about creating new message", payload);
this.app.models.message.create(messageObject).then((message) => {
// console.log("Mesage crewated", message);
const channelId = _.toString(_.get(message, 'channelId'));
this.app.models.channel.load(channelId).then((channel) => {
// console.log("got channel of the message created", channel);
const memberIds = _.get(channel, 'members', []);
_.each(memberIds, (memberId) => {
memberId = _.toString(memberId);
const memberConnections = this.connections.filter((c) => _.toString(c.userId) === memberId);
memberConnections.forEach((connection) => {
const ws = connection.ws;
this.send(ws, {
action: 'message_added',
payload: message,
})
})
});
})
// message created successful.
}).catch(err => {
// send back to the socket client who sent this messagse with error
const ws = userConnection.ws;
this.send(ws, {
action: 'create_message_error',
payload: payload,
})
})
}
break;
case 'create_channel':
let channel = payload;
const userId = userConnection.userId;
channel.userId = userId;
this.app.models.channel.create(channel).then((chanelObject) => {
// successful created channel ,
//console.log("Succesful created new channel", typeof userId, chanelObject);
// let send back to all members in this channel with new channel created
let memberConnections = [];
const memberIds = _.get(chanelObject, 'members', []);
// fetch all users has memberId
const query = {
_id: {$in: memberIds}
};
const queryOptions = {
_id: 1,
name: 1,
created: 1,
}
this.app.models.user.find(query, queryOptions).then((users) => {
chanelObject.users = users;
_.each(memberIds, (id) => {
const userId = id.toString();
const memberConnection = this.connections.filter((con) => `${con.userId}` === userId);
if (memberConnection.size) {
memberConnection.forEach((con) => {
const ws = con.ws;
const obj = {
action: 'channel_added',
payload: chanelObject,
}
// send to socket client matching userId in channel members.
this.send(ws, obj);
})
}
});
});
//const memberConnections = this.connections.filter((con) => `${con.userId}` = )
});
//console.log("Got new channel need to be created form client", channel);
break;
case 'auth':
const userTokenId = payload;
let connection = this.connections.get(socketId);
if (connection) {
// let find user with this token and verify it.
this.app.models.token.loadTokenAndUser(userTokenId).then((token) => {
const userId = token.userId;
connection.isAuthenticated = true;
connection.userId = `${userId}`;
this.connections = this.connections.set(socketId, connection);
// now send back to the client you are verified.
const obj = {
action: 'auth_success',
payload: 'You are verified',
}
this.send(connection.ws, obj);
//send to all socket clients connection
const userIdString = _.toString(userId);
this.sendToMembers(userIdString, {
action: 'user_online',
payload: userIdString,
});
this.app.models.user.updateUserStatus(userIdString, true);
}).catch((err) => {
// send back to socket client you are not logged.
const obj = {
action: 'auth_error',
payload: "An error authentication your account: " + userTokenId
};
this.send(connection.ws, obj);
})
}
break;
default:
break;
}
}
modelDidLoad() {
this.app.wss.on('connection', (ws) => {
const socketId = new ObjectID().toString();
//console.log("Somone connected to the server via socket.", socketId)
const clientConnection = {
_id: `${socketId}`,
ws: ws,
userId: null,
isAuthenticated: false,
}
// save this connection client to cache.
this.connections = this.connections.set(socketId, clientConnection);
// listen any message from websocket client.
ws.on('message', (msg) => {
//console.log("SERVER: message from a client", msg);
const message = this.decodeMesasge(msg);
this.doTheJob(socketId, message);
//console.log("SERVER: message from a client", msg);
});
ws.on('close', () => {
//console.log("Someone disconnected to the server", socketId);
const closeConnection = this.connections.get(socketId);
const userId = _.toString(_.get(closeConnection, 'userId', null));
// let remove this socket client from the cache collection.
this.connections = this.connections.remove(socketId);
if (userId) {
// now find all socket clients matching with userId
const userConnections = this.connections.filter((con) => _.toString(_.get(con, 'userId')) === userId);
if (userConnections.size === 0) {
// this mean no more socket clients is online with this userId. now user is offline.
this.sendToMembers(userId, {
action: 'user_offline',
payload: userId
});
// update user status into database
this.app.models.user.updateUserStatus(userId, false);
}
}
});
});
}
}
================================================
FILE: server/src/models/index.js
================================================
import User from './user'
import Token from './token'
import Connection from './connection'
import Channel from './channel'
import Message from "./message";
export default class Model{
constructor(app){
this.app = app;
this.user = new User(app);
this.token = new Token(app);
this.channel = new Channel(app);
this.message = new Message(app);
this.connection = new Connection(app);
}
}
================================================
FILE: server/src/models/message.js
================================================
import _ from 'lodash'
import {OrderedMap} from 'immutable'
import {ObjectID} from 'mongodb'
export default class Message {
constructor(app) {
this.app = app;
this.messages = new OrderedMap();
}
getChannelMessages(channelId, limit = 50, offset = 0){
return new Promise((resolve, reject) => {
channelId = new ObjectID(channelId);
const query = [
{
$lookup: {
from: 'users',
localField: 'userId',
foreignField: '_id',
as: 'user'
}
},
{
$match: {
'channelId': {$eq: channelId},
},
},
{
$project: {
_id: true,
channelId: true,
user: {$arrayElemAt: ['$user', 0]},
userId: true,
body: true,
created: true,
}
},
{
$project: {
_id: true,
channelId: true,
user: {_id: true, name: true, created: true, online: true},
userId: true,
body: true,
created: true,
}
},
{
$limit: limit
},
{
$skip: offset,
},
{
$sort: {created: -1}
}
];
this.app.db.collection('messages').aggregate(query, (err, results) => {
return err ? reject(err): resolve(results)
});
})
}
create(obj) {
return new Promise((resolve, reject) => {
let id = _.get(obj, '_id', null);
id = _.toString(id);
const userId = new ObjectID(_.get(obj, 'userId'));
const channelId = new ObjectID(_.get(obj, 'channelId'));
const message = {
_id: new ObjectID(id),
body: _.get(obj, 'body', ''),
userId: userId,
channelId: channelId,
created: new Date(),
};
this.app.db.collection('messages').insertOne(message, (err, info) => {
if(err){
return reject(err);
}
// let update lastMessgage field to channel
this.app.db.collection('channels').findOneAndUpdate({_id: channelId}, {
$set: {
lastMessage: _.get(message, 'body', ''),
updated: new Date(),
}
})
this.app.models.user.load(_.toString(userId)).then((user) => {
_.unset(user, 'password');
_.unset(user, 'email');
message.user = user;
return resolve(message);
}).catch((err) => {
return reject(err);
});
});
});
}
}
================================================
FILE: server/src/models/token.js
================================================
import _ from 'lodash'
import {ObjectID} from 'mongodb'
import {OrderedMap} from 'immutable'
export default class Token{
constructor(app){
this.app = app;
this.tokens = new OrderedMap();
}
logout(token){
return new Promise((resolve, reject) => {
const tokenId = _.toString(token._id);
// to remove token from cache
this.tokens = this.tokens.remove(tokenId);
// we have to delete this token id from tokens collection
this.app.db.collection('tokens').remove({_id: new ObjectID(tokenId)}, (err, info) => {
return err ? reject(err) : resolve(info);
});
})
}
loadTokenAndUser(id){
return new Promise((resolve, reject) => {
this.load(id).then((token) => {
const userId = `${token.userId}`
this.app.models.user.load(userId).then((user) => {
token.user = user;
return resolve(token);
}).catch(err => {
return reject(err);
});
}).catch((err) => {
return reject(err);
});
})
}
load(id = null){
id = `${id}`;
return new Promise((resolve, reject) => {
// first we check in cache if found dont need to query to database.
const tokenFromCache = this.tokens.get(id);
if(tokenFromCache){
return resolve(tokenFromCache);
}
this.findTokenById(id, (err, token) => {
if(!err && token){
const tokenId = token._id.toString();
this.tokens = this.tokens.set(tokenId, token);
}
return err ? reject(err) : resolve(token);
});
})
}
findTokenById(id, cb = () => {}){
//console.log("Begin query into database!!!!!!");
const idObject = new ObjectID(id);
const query = {_id: idObject}
this.app.db.collection('tokens').findOne(query, (err, result) => {
if(err || !result){
return cb({message: "Not found"}, null);
}
return cb(null, result);
})
}
create(userId){
const token = {
userId: userId,
created: new Date(),
}
return new Promise((resolve, reject) => {
this.app.db.collection('tokens').insertOne(token, (err, info) => {
return err ? reject(err) : resolve(token);
})
})
}
}
================================================
FILE: server/src/models/user.js
================================================
import _ from 'lodash'
import {isEmail} from '../helper'
import bcrypt from 'bcrypt'
import {ObjectID} from 'mongodb'
import {OrderedMap} from 'immutable'
const saltRound = 10;
export default class User {
constructor(app) {
this.app = app;
this.users = new OrderedMap();
}
updateUserStatus(userId, isOnline = false) {
return new Promise((resolve, reject) => {
// first update status of cache this.users
this.users = this.users.update(userId, (user) => {
if (user) {
user.online = isOnline;
}
return user;
});
const query = {_id: new ObjectID(userId)};
const updater = {$set: {online: isOnline}};
this.app.db.collection('users').update(query, updater, (err, info) => {
return err ? reject(err) : resolve(info);
});
})
}
find(query = {}, options = {}) {
return new Promise((resolve, reject) => {
this.app.db.collection('users').find(query, options).toArray((err, users) => {
return err ? reject(err) : resolve(users);
})
});
}
search(q = "") {
return new Promise((resolve, reject) => {
const regex = new RegExp(q, 'i');
const query = {
$or: [
{name: {$regex: regex}},
{email: {$regex: regex}},
],
};
this.app.db.collection('users').find(query, {
_id: true,
name: true,
created: true
}).toArray((err, results) => {
if (err || !results || !results.length) {
return reject({message: "User not found."})
}
return resolve(results);
});
});
}
login(user) {
const email = _.get(user, 'email', '');
const password = _.get(user, 'password', '');
return new Promise((resolve, reject) => {
if (!password || !email || !isEmail(email)) {
return reject({message: "An error login."})
}
// find in database with email
this.findUserByEmail(email, (err, result) => {
if (err) {
return reject({message: "Login Error."});
}
// if found user we have to compare the password hash and plain text.
const hashPassword = _.get(result, 'password');
const isMatch = bcrypt.compareSync(password, hashPassword);
if (!isMatch) {
return reject({message: "Login Error."});
}
// user login successful let creat new token save to token collection.
const userId = result._id;
this.app.models.token.create(userId).then((token) => {
token.user = result;
return resolve(token);
}).catch(err => {
return reject({message: "Login error"});
})
});
})
}
findUserByEmail(email, callback = () => {
}) {
this.app.db.collection('users').findOne({email: email}, (err, result) => {
if (err || !result) {
return callback({message: "User not found."})
}
return callback(null, result);
});
}
load(id) {
id = `${id}`;
return new Promise((resolve, reject) => {
// find in cache if found we return and dont nee to query db
const userInCache = this.users.get(id);
if (userInCache) {
return resolve(userInCache);
}
// if not found then we start query db
this.findUserById(id, (err, user) => {
if (!err && user) {
this.users = this.users.set(id, user);
}
return err ? reject(err) : resolve(user);
})
})
}
findUserById(id, callback = () => {
}) {
//console.log("Begin query in database");
if (!id) {
return callback({message: "User not found"}, null);
}
const userId = new ObjectID(id);
this.app.db.collection('users').findOne({_id: userId}, (err, result) => {
if (err || !result) {
return callback({message: "User not found"});
}
return callback(null, result);
});
}
beforeSave(user, callback = () => {
}) {
// first is validate user object before save to user collection.
let errors = [];
const fields = ['name', 'email', 'password'];
const validations = {
name: {
errorMesage: 'Name is required',
do: () => {
const name = _.get(user, 'name', '');
return name.length;
}
},
email: {
errorMesage: 'Email is not correct',
do: () => {
const email = _.get(user, 'email', '');
if (!email.length || !isEmail(email)) {
return false;
}
return true;
}
},
password: {
errorMesage: 'Password is required and more than 3 characters',
do: () => {
const password = _.get(user, 'password', '');
if (!password.length || password.length < 3) {
return false;
}
return true;
}
}
}
// loop all fields to check if valid or not.
fields.forEach((field) => {
const fieldValidation = _.get(validations, field);
if (fieldValidation) {
// do check/
const isValid = fieldValidation.do();
const msg = fieldValidation.errorMesage;
if (!isValid) {
errors.push(msg);
}
}
});
if (errors.length) {
// this is not pass of the validation.
const err = _.join(errors, ',');
return callback(err, null);
}
// check email is exist in db or not
const email = _.toLower(_.trim(_.get(user, 'email', '')));
this.app.db.collection('users').findOne({email: email}, (err, result) => {
if (err || result) {
return callback({message: "Email is already exist"}, null);
}
// return callback with succes checked.
const password = _.get(user, 'password');
const hashPassword = bcrypt.hashSync(password, saltRound);
const userFormatted = {
name: `${_.trim(_.get(user, 'name'))}`,
email: email,
password: hashPassword,
created: new Date(),
};
return callback(null, userFormatted);
});
}
create(user) {
const db = this.app.db;
console.log("User:", user)
return new Promise((resolve, reject) => {
this.beforeSave(user, (err, user) => {
console.log("After validation: ", err, user);
if (err) {
return reject(err);
}
// insert new user object to users collections
db.collection('users').insertOne(user, (err, info) => {
// check if error return error to user
if (err) {
return reject({message: "An error saving user."});
}
// otherwise return user object to user.
const userId = _.get(user, '_id').toString(); // this is OBJET ID
this.users = this.users.set(userId, user);
return resolve(user);
});
});
});
}
}
================================================
FILE: server/src/www/index.html
================================================
<html>
<body>
<h1>Welcome to my website.</h1>
</body>
</html>
gitextract_xpe_08_e/
├── .gitignore
├── README.md
├── app/
│ ├── .gitignore
│ ├── Dockerfile
│ ├── README.md
│ ├── docker-compose.yml
│ ├── package.json
│ ├── public/
│ │ ├── 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.scss
│ ├── helpers/
│ │ ├── index.js
│ │ └── objectid.js
│ ├── index.js
│ ├── realtime.js
│ ├── registerServiceWorker.js
│ ├── service.js
│ └── store.js
├── 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
SYMBOL INDEX (134 symbols across 19 files)
FILE: app/src/components/app.js
class App (line 5) | class App extends Component{
method constructor (line 7) | constructor(props){
method render (line 16) | render(){
FILE: app/src/components/messenger.js
class Messenger (line 11) | class Messenger extends Component {
method constructor (line 13) | constructor(props) {
method renderChannelAvatars (line 33) | renderChannelAvatars(channel){
method renderChannelTitle (line 52) | renderChannelTitle(channel = null) {
method _onCreateChannel (line 79) | _onCreateChannel() {
method scrollMessagesToBottom (line 106) | scrollMessagesToBottom() {
method renderMessage (line 114) | renderMessage(message) {
method handleSend (line 127) | handleSend() {
method _onResize (line 162) | _onResize() {
method componentDidUpdate (line 169) | componentDidUpdate() {
method componentDidMount (line 175) | componentDidMount() {
method componentWillUnmount (line 184) | componentWillUnmount() {
method render (line 190) | render() {
FILE: app/src/components/search-user.js
class SearchUser (line 4) | class SearchUser extends Component{
method constructor (line 7) | constructor(props){
method handleOnClick (line 17) | handleOnClick(user){
method render (line 24) | render(){
FILE: app/src/components/user-bar.js
class UserBar (line 8) | class UserBar extends Component {
method constructor (line 10) | constructor(props) {
method render (line 21) | render() {
FILE: app/src/components/user-form.js
class UserForm (line 6) | class UserForm extends Component {
method constructor (line 8) | constructor(props) {
method onClickOutside (line 29) | onClickOutside(event) {
method componentDidMount (line 41) | componentDidMount() {
method componentWillUnmount (line 47) | componentWillUnmount() {
method onSubmit (line 53) | onSubmit(event) {
method onTextFieldChange (line 113) | onTextFieldChange(event) {
method render (line 129) | render() {
FILE: app/src/components/user-menu.js
class UserMenu (line 4) | class UserMenu extends Component{
method constructor (line 6) | constructor(props){
method onClickOutside (line 18) | onClickOutside(event){
method componentDidMount (line 30) | componentDidMount(){
method componentWillUnmount (line 35) | componentWillUnmount(){
method render (line 43) | render(){
FILE: app/src/realtime.js
class Realtime (line 5) | class Realtime {
method constructor (line 8) | constructor(store) {
method reconnect (line 19) | reconnect(){
method decodeMessage (line 36) | decodeMessage(msg) {
method readMessage (line 53) | readMessage(msg) {
method onUpdateUserStatus (line 100) | onUpdateUserStatus(userId, isOnline = false){
method onAddMessage (line 120) | onAddMessage(payload, notify = false){
method onAddChannel (line 151) | onAddChannel(payload) {
method send (line 203) | send(msg = {}) {
method authentication (line 216) | authentication() {
method connect (line 234) | connect() {
FILE: app/src/registerServiceWorker.js
function register (line 21) | function register() {
function registerValidSW (line 46) | function registerValidSW(swUrl) {
function checkValidServiceWorker (line 75) | function checkValidServiceWorker(swUrl) {
function unregister (line 102) | function unregister() {
FILE: app/src/service.js
class Service (line 6) | class Service{
method get (line 8) | get(endpoint, options = null){
method post (line 15) | post(endpoint = "", data = {}, options = {headers: {'Content-Type': 'a...
FILE: app/src/store.js
class Store (line 6) | class Store {
method constructor (line 7) | constructor(appComponent) {
method isConnected (line 33) | isConnected(){
method fetchUserChannels (line 37) | fetchUserChannels(){
method addUserToCache (line 72) | addUserToCache(user) {
method getUserTokenId (line 84) | getUserTokenId() {
method loadUserAvatar (line 88) | loadUserAvatar(user) {
method startSearchUsers (line 93) | startSearchUsers(q = "") {
method setUserToken (line 132) | setUserToken(accessToken) {
method getTokenFromLocalStore (line 147) | getTokenFromLocalStore() {
method getUserFromLocalStorage (line 172) | getUserFromLocalStorage() {
method setCurrentUser (line 217) | setCurrentUser(user) {
method clearCacheData (line 237) | clearCacheData(){
method signOut (line 243) | signOut() {
method register (line 270) | register(user){
method login (line 287) | login(email = null, password = null) {
method removeMemberFromChannel (line 340) | removeMemberFromChannel(channel = null, user = null) {
method addUserToChannel (line 357) | addUserToChannel(channelId, userId) {
method getSearchUsers (line 372) | getSearchUsers() {
method onCreateNewChannel (line 377) | onCreateNewChannel(channel = {}) {
method getCurrentUser (line 387) | getCurrentUser() {
method fetchChannelMessages (line 393) | fetchChannelMessages(channelId){
method setActiveChannelId (line 436) | setActiveChannelId(id) {
method getActiveChannel (line 447) | getActiveChannel() {
method setMessage (line 454) | setMessage(message, notify = false) {
method addMessage (line 489) | addMessage(id, message = {}) {
method getMessages (line 543) | getMessages() {
method getMessagesFromChannel (line 548) | getMessagesFromChannel(channel) {
method getMembersFromChannel (line 570) | getMembersFromChannel(channel) {
method addChannel (line 596) | addChannel(index, channel = {}) {
method getChannels (line 602) | getChannels() {
method update (line 614) | update() {
FILE: server/src/app-router.js
constant START_TIME (line 5) | const START_TIME = new Date();
class AppRouter (line 7) | class AppRouter {
method constructor (line 10) | constructor(app) {
method setupRouter (line 21) | setupRouter() {
FILE: server/src/database.js
constant URL (line 3) | const URL = 'mongodb://mongo:27017';
class Database (line 6) | class Database{
method connect (line 8) | connect(){
FILE: server/src/index.js
constant PORT (line 12) | const PORT = 3001;
FILE: server/src/models/channel.js
class Channel (line 6) | class Channel {
method constructor (line 8) | constructor(app) {
method aggregate (line 16) | aggregate(q){
method find (line 32) | find(q, options = {}){
method load (line 48) | load(id) {
method findById (line 85) | findById(id){
method create (line 104) | create(obj) {
FILE: server/src/models/connection.js
class Connection (line 5) | class Connection {
method constructor (line 7) | constructor(app) {
method decodeMesasge (line 17) | decodeMesasge(msg) {
method sendToMembers (line 37) | sendToMembers(userId, obj) {
method sendAll (line 111) | sendAll(obj) {
method send (line 123) | send(ws, obj) {
method doTheJob (line 130) | doTheJob(socketId, msg) {
method modelDidLoad (line 338) | modelDidLoad() {
FILE: server/src/models/index.js
class Model (line 7) | class Model{
method constructor (line 9) | constructor(app){
FILE: server/src/models/message.js
class Message (line 5) | class Message {
method constructor (line 7) | constructor(app) {
method getChannelMessages (line 12) | getChannelMessages(channelId, limit = 50, offset = 0){
method create (line 78) | create(obj) {
FILE: server/src/models/token.js
class Token (line 5) | class Token{
method constructor (line 7) | constructor(app){
method logout (line 16) | logout(token){
method loadTokenAndUser (line 33) | loadTokenAndUser(id){
method load (line 62) | load(id = null){
method findTokenById (line 96) | findTokenById(id, cb = () => {}){
method create (line 118) | create(userId){
FILE: server/src/models/user.js
class User (line 9) | class User {
method constructor (line 11) | constructor(app) {
method updateUserStatus (line 19) | updateUserStatus(userId, isOnline = false) {
method find (line 45) | find(query = {}, options = {}) {
method search (line 58) | search(q = "") {
method login (line 91) | login(user) {
method findUserByEmail (line 153) | findUserByEmail(email, callback = () => {
method load (line 171) | load(id) {
method findUserById (line 204) | findUserById(id, callback = () => {
method beforeSave (line 228) | beforeSave(user, callback = () => {
method create (line 336) | create(user) {
Condensed preview — 45 files, each showing path, character count, and a content snippet. Download the .json file or copy for the full structured content (144K chars).
[
{
"path": ".gitignore",
"chars": 250,
"preview": ".idea\n.DS_Store\n.env.local\n.env.development.local\n.env.test.local\n.env.production.local\n\nnpm-debug.log*\nyarn-debug.log*\n"
},
{
"path": "README.md",
"chars": 1661,
"preview": "# nodejs-reactjs-chatapp\n\nCreate messenger chat application use Nodejs Expressjs, Reactjs.\n\n## Screenshot:\n\n<img src=\"ht"
},
{
"path": "app/.gitignore",
"chars": 285,
"preview": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/cov"
},
{
"path": "app/Dockerfile",
"chars": 209,
"preview": "FROM node:11.12.0\n\n# Install a bunch of node modules that are commonly used.\n#ADD package.json /usr/app/\nADD . /usr/app/"
},
{
"path": "app/README.md",
"chars": 54,
"preview": "## Start app\n\n```\n npm install\n```\n\n```\nnpm start\n```"
},
{
"path": "app/docker-compose.yml",
"chars": 96,
"preview": "version: '3'\nservices:\n app:\n build: .\n ports:\n - \"3000:3000\"\n command: npm start"
},
{
"path": "app/package.json",
"chars": 473,
"preview": "{\n \"name\": \"my-app\",\n \"version\": \"0.1.0\",\n \"private\": true,\n \"dependencies\": {\n \"axios\": \"^0.17.1\",\n \"classnam"
},
{
"path": "app/public/index.html",
"chars": 1590,
"preview": "<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\">\n <meta name=\"viewport\" content=\"width=device-wid"
},
{
"path": "app/public/manifest.json",
"chars": 317,
"preview": "{\n \"short_name\": \"React App\",\n \"name\": \"Create React App Sample\",\n \"icons\": [\n {\n \"src\": \"favicon.ico\",\n "
},
{
"path": "app/src/components/app.js",
"chars": 368,
"preview": "import React, {Component} from 'react'\nimport Store from '../store'\nimport Messenger from './messenger'\n\nexport default "
},
{
"path": "app/src/components/messenger.js",
"chars": 12517,
"preview": "import React, {Component} from 'react'\nimport classNames from 'classnames'\nimport {OrderedMap} from 'immutable'\nimport _"
},
{
"path": "app/src/components/search-user.js",
"chars": 729,
"preview": "import React, {Component} from 'react'\nimport _ from 'lodash'\n\nexport default class SearchUser extends Component{\n\n\n\tcon"
},
{
"path": "app/src/components/user-bar.js",
"chars": 1863,
"preview": "import React, {Component} from 'react'\nimport _ from 'lodash'\nimport avatar from '../images/avatar.png'\nimport UserForm "
},
{
"path": "app/src/components/user-form.js",
"chars": 4449,
"preview": "import React, {Component} from 'react'\nimport _ from 'lodash'\nimport classNames from 'classnames'\n\n\nexport default class"
},
{
"path": "app/src/components/user-menu.js",
"chars": 1098,
"preview": "import React,{Component} from 'react'\n\n\nexport default class UserMenu extends Component{\n\n\tconstructor(props){\n\t\tsuper(p"
},
{
"path": "app/src/config.js",
"chars": 321,
"preview": "export const production = false; // set it to true when deploy to the server\n\nconst domain = production ? '139.59.227.12"
},
{
"path": "app/src/css/_font.scss",
"chars": 1199,
"preview": "@charset \"UTF-8\";\n\n@font-face {\n font-family: \"chatapp\";\n src:url(\"./fonts/chatapp.eot\");\n src:url(\"./fonts/chatapp.e"
},
{
"path": "app/src/css/_variable.scss",
"chars": 185,
"preview": "$header-height: 50px;\n$left-sidebar-width: 200px;\n$right-sidebar-width: 300px;\n$border-color: rgba(0, 0, 0, 0.05);\n$prim"
},
{
"path": "app/src/css/app.css",
"chars": 13136,
"preview": "@import \"https://fonts.googleapis.com/css?family=Open+Sans:400,600\";\n@font-face {\n font-family: \"chatapp\";\n src: url(\""
},
{
"path": "app/src/css/app.scss",
"chars": 10697,
"preview": "@import \"font\";\n@import \"https://fonts.googleapis.com/css?family=Open+Sans:400,600\";\n@import 'variable';\n\nbody, html {\n "
},
{
"path": "app/src/helpers/index.js",
"chars": 0,
"preview": ""
},
{
"path": "app/src/helpers/objectid.js",
"chars": 11017,
"preview": "/**\n * Machine id.\n *\n * Create a random 3-byte value (i.e. unique for this\n * process). Other drivers use a md5 of the "
},
{
"path": "app/src/index.js",
"chars": 273,
"preview": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './components/app';\nimport './css/app.css'\n"
},
{
"path": "app/src/realtime.js",
"chars": 5619,
"preview": "import _ from 'lodash'\nimport {OrderedMap} from 'immutable'\nimport {websocketUrl} from './config'\n\nexport default class "
},
{
"path": "app/src/registerServiceWorker.js",
"chars": 4021,
"preview": "// In production, we register a service worker to serve assets from local cache.\n\n// This lets the app load faster on su"
},
{
"path": "app/src/service.js",
"chars": 406,
"preview": "import axios from 'axios'\nimport {apiUrl} from './config'\n\nconst apiURL = apiUrl;\n\nexport default class Service{\n\n\tget(e"
},
{
"path": "app/src/store.js",
"chars": 13514,
"preview": "import {OrderedMap} from 'immutable'\nimport _ from 'lodash'\nimport Service from './service'\nimport Realtime from './real"
},
{
"path": "deployment-to-digitalocean-hosting.md",
"chars": 7324,
"preview": "# Deploy Reactjs, Nodejs Chat app to DigitalOcean hosting (Ubuntu VPS)\n\n## Get DigitalOcean account\n\nI have been using <"
},
{
"path": "server/Dockerfile",
"chars": 789,
"preview": "FROM ubuntu:18.04\n#RUN apk add --update \\ libc6-compat\n# Install a bunch of node modules that are commonly used.\n#ADD pa"
},
{
"path": "server/docker-compose.yml",
"chars": 196,
"preview": "version: '3'\nservices:\n server:\n build: .\n ports:\n - \"3001:3001\"\n depends_on:\n - mongo\n command: "
},
{
"path": "server/package.json",
"chars": 1150,
"preview": "{\n \"name\": \"chatapp\",\n \"version\": \"1.0.0\",\n \"description\": \"Use websocket in application.\",\n \"main\": \"index.js\",\n \""
},
{
"path": "server/src/app-router.js",
"chars": 9282,
"preview": "import moment from 'moment';\nimport _ from 'lodash'\n\n\nexport const START_TIME = new Date();\n\nexport default class AppRou"
},
{
"path": "server/src/database.js",
"chars": 282,
"preview": "import {MongoClient} from 'mongodb'\n\nconst URL = 'mongodb://mongo:27017';\n\n\nexport default class Database{\n\n\tconnect(){\n"
},
{
"path": "server/src/helper.js",
"chars": 302,
"preview": "export const isEmail = (emaill) => {\n\n\n\tconst regex = /^(([^<>()\\[\\]\\\\.,;:\\s@\"]+(\\.[^<>()\\[\\]\\\\.,;:\\s@\"]+)*)|(\".+\"))@((\\"
},
{
"path": "server/src/index.js",
"chars": 1137,
"preview": "import http from 'http';\nimport express from 'express';\nimport cors from 'cors';\nimport bodyParser from 'body-parser';\ni"
},
{
"path": "server/src/models/channel.js",
"chars": 3012,
"preview": "import _ from 'lodash'\nimport {toString} from '../helper'\nimport {ObjectID} from 'mongodb'\nimport {OrderedMap} from 'imm"
},
{
"path": "server/src/models/connection.js",
"chars": 10909,
"preview": "import {OrderedMap} from 'immutable'\nimport {ObjectID} from 'mongodb'\nimport _ from 'lodash'\n\nexport default class Conne"
},
{
"path": "server/src/models/index.js",
"chars": 402,
"preview": "import User from './user'\nimport Token from './token'\nimport Connection from './connection'\nimport Channel from './chann"
},
{
"path": "server/src/models/message.js",
"chars": 3369,
"preview": "import _ from 'lodash'\nimport {OrderedMap} from 'immutable'\nimport {ObjectID} from 'mongodb'\n\nexport default class Messa"
},
{
"path": "server/src/models/token.js",
"chars": 2163,
"preview": "import _ from 'lodash'\nimport {ObjectID} from 'mongodb'\nimport {OrderedMap} from 'immutable'\n\nexport default class Token"
},
{
"path": "server/src/models/user.js",
"chars": 8310,
"preview": "import _ from 'lodash'\nimport {isEmail} from '../helper'\nimport bcrypt from 'bcrypt'\nimport {ObjectID} from 'mongodb'\nim"
},
{
"path": "server/src/www/index.html",
"chars": 63,
"preview": "<html>\n\n<body>\n\n<h1>Welcome to my website.</h1>\n</body>\n</html>"
}
]
// ... and 3 more files (download for full content)
About this extraction
This page contains the full source code of the tabvn/nodejs-reactjs-chatapp GitHub repository, extracted and formatted as plain text for AI agents and large language models (LLMs). The extraction includes 45 files (131.9 KB), approximately 31.9k tokens, and a symbol index with 134 extracted functions, classes, methods, constants, and types. Use this with OpenClaw, Claude, ChatGPT, Cursor, Windsurf, or any other AI tool that accepts text input. You can copy the full output to your clipboard or download it as a .txt file.
Extracted by GitExtract — free GitHub repo to text converter for AI. Built by Nikandr Surkov.