[
  {
    "path": ".gitignore",
    "content": ".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*\nyarn-error.log*\n\nserver/node_modules\napp/node_modules\nserver/dist\nserver/package-lock.json\n\napp/node_modules\napp/package-lock.json"
  },
  {
    "path": "README.md",
    "content": "# nodejs-reactjs-chatapp\n\nCreate messenger chat application use Nodejs Expressjs, Reactjs.\n\n## Screenshot:\n\n<img src=\"https://lh3.googleusercontent.com/bk7OOm_rDDP8TgKK3KYj5lEVBc4FptkWBlGce6_pRjBj2TMTSQD6jgTdxyU0vqI30AaacSntUuhzkiltph_jMJYI4bUrjN3AVcoyDp-HC0aR-iXZ_zoLhR9cfeI9gdifcnPp8TlRpQ=w2548-h1318-no\" />\n\n## Server\n\n``` \ncd server \n```\n```\nnpm install\n```\n\n```\nnpm run dev\n```\n### Reactjs App development\n\n```\ncd app\n```\n\n```\nnpm start\n```\n\n### Reactjs App development using docker-compose\n\nThe 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:\n``` \ncd server \n```\n```\ndocker-compose up\n```\nAt this moment the server application side will be running.\n\nNow it's time to run application front end. Open a new terminal (window or tab) and in the project folder use the following commands:\n``` \ncd app \n```\n```\ndocker-compose up\n```\n\nAttention: Deppending on the way you have installed the docker in your compile you may use **sudo** command to run docker, for example:\n``` \nsudo docker-compose up\n```\n\nFor more docker informations and how to install access https://www.docker.com/ .\n \n## Tutorials\n* Checkout the video toturials list: https://www.youtube.com/playlist?list=PLFaW_8zE4amPaLyz5AyVT8B_wfOYwd8x8\n* My Facebook: https://www.facebook.com/TabvnGroup/\n* Youtube Chanel: https://youtube.com/tabvn\n\n\n## Deploy Node.js React.js to DigitalOcean.com Ubuntu 16.04 Cloud VPS \n\n* <a href=\"https://github.com/tabvn/nodejs-reactjs-chatapp/blob/master/deployment-to-digitalocean-hosting.md\">Document</a>\n* Video: https://www.youtube.com/watch?v=wJsH45eWNBo\n\n"
  },
  {
    "path": "app/.gitignore",
    "content": "# See https://help.github.com/ignore-files/ for more about ignoring files.\n\n# dependencies\n/node_modules\n\n# testing\n/coverage\n\n# production\n/build\n\n# misc\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*\nyarn-error.log*\n"
  },
  {
    "path": "app/Dockerfile",
    "content": "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/\n\nEXPOSE 80\nENV BIND_HOST=0.0.0.0\nCMD [\"npm\", \"start\"]\nWORKDIR /usr/app\n\nRUN npm install\n"
  },
  {
    "path": "app/README.md",
    "content": "## Start app\n\n```\n  npm install\n```\n\n```\nnpm start\n```"
  },
  {
    "path": "app/docker-compose.yml",
    "content": "version: '3'\nservices:\n  app:\n    build: .\n    ports:\n      - \"3000:3000\"\n    command: npm start"
  },
  {
    "path": "app/package.json",
    "content": "{\n  \"name\": \"my-app\",\n  \"version\": \"0.1.0\",\n  \"private\": true,\n  \"dependencies\": {\n    \"axios\": \"^0.17.1\",\n    \"classnames\": \"^2.2.5\",\n    \"immutable\": \"^3.8.2\",\n    \"lodash\": \"^4.17.4\",\n    \"moment\": \"^2.19.2\",\n    \"react\": \"^16.1.1\",\n    \"react-dom\": \"^16.1.1\",\n    \"react-scripts\": \"1.0.17\"\n  },\n  \"scripts\": {\n    \"start\": \"react-scripts start\",\n    \"build\": \"react-scripts build\",\n    \"test\": \"react-scripts test --env=jsdom\",\n    \"eject\": \"react-scripts eject\"\n  }\n}\n"
  },
  {
    "path": "app/public/index.html",
    "content": "<!DOCTYPE html>\n<html lang=\"en\">\n  <head>\n    <meta charset=\"utf-8\">\n    <meta name=\"viewport\" content=\"width=device-width, initial-scale=1, shrink-to-fit=no\">\n    <meta name=\"theme-color\" content=\"#000000\">\n    <!--\n      manifest.json provides metadata used when your web app is added to the\n      homescreen on Android. See https://developers.google.com/web/fundamentals/engage-and-retain/web-app-manifest/\n    -->\n    <link rel=\"manifest\" href=\"%PUBLIC_URL%/manifest.json\">\n    <link rel=\"shortcut icon\" href=\"%PUBLIC_URL%/favicon.ico\">\n    <!--\n      Notice the use of %PUBLIC_URL% in the tags above.\n      It will be replaced with the URL of the `public` folder during the build.\n      Only files inside the `public` folder can be referenced from the HTML.\n\n      Unlike \"/favicon.ico\" or \"favicon.ico\", \"%PUBLIC_URL%/favicon.ico\" will\n      work correctly both with client-side routing and a non-root public URL.\n      Learn how to configure a non-root public URL by running `npm run build`.\n    -->\n    <title>React App</title>\n  </head>\n  <body>\n    <noscript>\n      You need to enable JavaScript to run this app.\n    </noscript>\n    <div id=\"root\"></div>\n    <!--\n      This HTML file is a template.\n      If you open it directly in the browser, you will see an empty page.\n\n      You can add webfonts, meta tags, or analytics to this file.\n      The build step will place the bundled scripts into the <body> tag.\n\n      To begin the development, run `npm start` or `yarn start`.\n      To create a production bundle, use `npm run build` or `yarn build`.\n    -->\n  </body>\n</html>\n"
  },
  {
    "path": "app/public/manifest.json",
    "content": "{\n  \"short_name\": \"React App\",\n  \"name\": \"Create React App Sample\",\n  \"icons\": [\n    {\n      \"src\": \"favicon.ico\",\n      \"sizes\": \"64x64 32x32 24x24 16x16\",\n      \"type\": \"image/x-icon\"\n    }\n  ],\n  \"start_url\": \"./index.html\",\n  \"display\": \"standalone\",\n  \"theme_color\": \"#000000\",\n  \"background_color\": \"#ffffff\"\n}\n"
  },
  {
    "path": "app/src/components/app.js",
    "content": "import React, {Component} from 'react'\nimport Store from '../store'\nimport Messenger from './messenger'\n\nexport default class App extends Component{\n\n\tconstructor(props){\n\t\tsuper(props);\n\n\t\tthis.state = {\n\n\t\t\tstore: new Store(this),\n\t\t}\n\t}\n\n\trender(){\n\n\t\tconst {store} = this.state;\n\t\treturn <div className=\"app-wrapper\">\n\t\t\t\t<Messenger store={store} />\n\t\t\t</div>\n\t}\n}"
  },
  {
    "path": "app/src/components/messenger.js",
    "content": "import React, {Component} from 'react'\nimport classNames from 'classnames'\nimport {OrderedMap} from 'immutable'\nimport _ from 'lodash'\nimport {ObjectID} from '../helpers/objectid'\nimport SearchUser from './search-user'\nimport moment from 'moment'\nimport UserBar from './user-bar'\n\n\nexport default class Messenger extends Component {\n\n    constructor(props) {\n\n        super(props);\n\n        this.state = {\n            height: window.innerHeight,\n            newMessage: 'Hello there...',\n            searchUser: \"\",\n            showSearchUser: false,\n        }\n\n        this._onResize = this._onResize.bind(this);\n        this.handleSend = this.handleSend.bind(this)\n        this.renderMessage = this.renderMessage.bind(this);\n        this.scrollMessagesToBottom = this.scrollMessagesToBottom.bind(this)\n        this._onCreateChannel = this._onCreateChannel.bind(this);\n        this.renderChannelTitle = this.renderChannelTitle.bind(this)\n        this.renderChannelAvatars = this.renderChannelAvatars.bind(this);\n    }\n\n    renderChannelAvatars(channel){\n        const {store} = this.props;\n\n        const members = store.getMembersFromChannel(channel);\n\n        const maxDisplay = 4;\n        const total = members.size > maxDisplay ? maxDisplay : members.size;\n\n        const avatars = members.map((user, index) => {\n\n\n\n            return index < maxDisplay ?  <img key={index} src={_.get(user, 'avatar')} alt={_.get(user, 'name')} /> : null\n\n        });\n\n\n        return <div className={classNames('channel-avatars', `channel-avatars-${total}`)}>{avatars}</div>\n    }\n    renderChannelTitle(channel = null) {\n\n        if (!channel) {\n            return null;\n        }\n        const {store} = this.props;\n\n        const members = store.getMembersFromChannel(channel);\n\n\n        const names = [];\n\n        members.forEach((user) => {\n\n            const name = _.get(user, 'name');\n            names.push(name);\n        })\n\n        let title = _.join(names, ',');\n\n        if (!title && _.get(channel, 'isNew')) {\n            title = 'New message';\n        }\n\n        return <h2>{title}</h2>\n    }\n\n    _onCreateChannel() {\n\n        const {store} = this.props;\n\n        const currentUser = store.getCurrentUser();\n        const currentUserId = _.get(currentUser, '_id');\n\n        const channelId = new ObjectID().toString();\n        const channel = {\n            _id: channelId,\n            title: '',\n            lastMessage: \"\",\n            members: new OrderedMap(),\n            messages: new OrderedMap(),\n            isNew: true,\n            userId: currentUserId,\n            created: new Date(),\n        };\n\n        channel.members = channel.members.set(currentUserId, true);\n\n\n        store.onCreateNewChannel(channel);\n\n\n    }\n\n    scrollMessagesToBottom() {\n\n        if (this.messagesRef) {\n\n            this.messagesRef.scrollTop = this.messagesRef.scrollHeight;\n        }\n    }\n\n    renderMessage(message) {\n\n        const text = _.get(message, 'body', '');\n\n        const html = _.split(text, '\\n').map((m, key) => {\n\n            return <p key={key} dangerouslySetInnerHTML={{__html: m}}/>\n        })\n\n\n        return html;\n    }\n\n    handleSend() {\n\n        const {newMessage} = this.state;\n        const {store} = this.props;\n\n\n        // create new message\n\n        if (_.trim(newMessage).length) {\n\n            const messageId = new ObjectID().toString();\n            const channel = store.getActiveChannel();\n            const channelId = _.get(channel, '_id', null);\n            const currentUser = store.getCurrentUser();\n\n            const message = {\n                _id: messageId,\n                channelId: channelId,\n                body: newMessage,\n                userId: _.get(currentUser, '_id'),\n                me: true,\n\n            };\n\n\n            store.addMessage(messageId, message);\n\n            this.setState({\n                newMessage: '',\n            })\n        }\n\n\n    }\n\n    _onResize() {\n\n        this.setState({\n            height: window.innerHeight\n        });\n    }\n\n    componentDidUpdate() {\n\n\n        this.scrollMessagesToBottom();\n    }\n\n    componentDidMount() {\n\n\n        window.addEventListener('resize', this._onResize);\n\n\n    }\n\n\n    componentWillUnmount() {\n\n        window.removeEventListener('resize', this._onResize)\n\n    }\n\n    render() {\n\n        const {store} = this.props;\n\n        const {height} = this.state;\n\n        const style = {\n            height: height,\n        };\n\n\n        const activeChannel = store.getActiveChannel();\n        const messages = store.getMessagesFromChannel(activeChannel); //store.getMessages();\n        const channels = store.getChannels();\n        const members = store.getMembersFromChannel(activeChannel);\n\n\n        return (\n            <div style={style} className=\"app-messenger\">\n                <div className=\"header\">\n                    <div className=\"left\">\n                        <button className=\"left-action\"><i className=\"icon-settings-streamline-1\"/></button>\n                        <button onClick={this._onCreateChannel} className=\"right-action\"><i\n                            className=\"icon-edit-modify-streamline\"/></button>\n                        <h2>Messenger</h2>\n                    </div>\n                    <div className=\"content\">\n\n                        {_.get(activeChannel, 'isNew') ? <div className=\"toolbar\">\n                            <label>To:</label>\n                            {\n                                members.map((user, key) => {\n\n                                    return <span onClick={() => {\n\n                                        store.removeMemberFromChannel(activeChannel, user);\n\n                                    }} key={key}>{_.get(user, 'name')}</span>\n                                })\n                            }\n                            <input placeholder=\"Type name of person...\" onChange={(event) => {\n\n                                const searchUserText = _.get(event, 'target.value');\n\n                                //console.log(\"searching for user with name: \", searchUserText)\n\n                                this.setState({\n                                    searchUser: searchUserText,\n                                    showSearchUser: true,\n                                }, () => {\n\n\n                                    store.startSearchUsers(searchUserText);\n                                });\n\n\n                            }} type=\"text\" value={this.state.searchUser}/>\n\n                            {this.state.showSearchUser ? <SearchUser\n                                onSelect={(user) => {\n\n                                    this.setState({\n                                        showSearchUser: false,\n                                        searchUser: '',\n\n                                    }, () => {\n\n\n                                        const userId = _.get(user, '_id');\n                                        const channelId = _.get(activeChannel, '_id');\n\n                                        store.addUserToChannel(channelId, userId);\n\n                                    });\n\n\n                                }}\n                                store={store}/> : null}\n\n                        </div> : this.renderChannelTitle(activeChannel)}\n\n\n                    </div>\n                    <div className=\"right\">\n\n                        <UserBar store={store}/>\n\n                    </div>\n                </div>\n                <div className=\"main\">\n                    <div className=\"sidebar-left\">\n\n                        <div className=\"chanels\">\n\n                            {channels.map((channel, key) => {\n\n                                return (\n                                    <div onClick={(key) => {\n\n                                        store.setActiveChannelId(channel._id);\n\n                                    }} key={channel._id}\n                                         className={classNames('chanel', {'notify': _.get(channel, 'notify') === true},{'active': _.get(activeChannel, '_id') === _.get(channel, '_id', null)})}>\n                                        <div className=\"user-image\">\n                                           {this.renderChannelAvatars(channel)}\n                                        </div>\n                                        <div className=\"chanel-info\">\n                                            {this.renderChannelTitle(channel)}\n                                            <p>{channel.lastMessage}</p>\n                                        </div>\n\n                                    </div>\n                                )\n\n                            })}\n\n\n                        </div>\n                    </div>\n                    <div className=\"content\">\n                        <div ref={(ref) => this.messagesRef = ref} className=\"messages\">\n\n                            {messages.map((message, index) => {\n\n                                const user = _.get(message, 'user');\n\n\n                                return (\n                                    <div key={index} className={classNames('message', {'me': message.me})}>\n                                        <div className=\"message-user-image\">\n                                            <img src={_.get(user, 'avatar')} alt=\"\"/>\n                                        </div>\n                                        <div className=\"message-body\">\n                                            <div\n                                                className=\"message-author\">{message.me ? 'You ' : _.get(message, 'user.name')} says:\n                                            </div>\n                                            <div className=\"message-text\">\n                                                {this.renderMessage(message)}\n                                            </div>\n                                        </div>\n                                    </div>\n                                )\n\n\n                            })}\n\n\n                        </div>\n\n                        {activeChannel && members.size > 0 ? <div className=\"messenger-input\">\n\n                            <div className=\"text-input\">\n\t\t\t\t\t\t\t\t\t\t<textarea onKeyUp={(event) => {\n\n\n                                            if (event.key === 'Enter' && !event.shiftKey) {\n                                                this.handleSend();\n                                            }\n\n\n                                        }} onChange={(event) => {\n\n\n                                            this.setState({newMessage: _.get(event, 'target.value')});\n\n                                        }} value={this.state.newMessage} placeholder=\"Write your messsage...\"/>\n                            </div>\n                            <div className=\"actions\">\n                                <button onClick={this.handleSend} className=\"send\">Send</button>\n                            </div>\n                        </div> : null}\n\n\n                    </div>\n                    <div className=\"sidebar-right\">\n\n                        {members.size > 0 ? <div><h2 className=\"title\">Members</h2>\n                            <div className=\"members\">\n\n                                {members.map((member, key) => {\n\n\n                                    const isOnline = _.get(member, 'online', false);\n\n                                    return (\n                                        <div key={key} className=\"member\">\n                                            <div className=\"user-image\">\n                                                <img src={_.get(member, 'avatar')} alt=\"\"/>\n                                                <span className={classNames('user-status', {'online': isOnline})} />\n                                            </div>\n                                            <div className=\"member-info\">\n                                                <h2>{member.name} - <span className={classNames('user-status', {'online': isOnline})}>{isOnline ? 'Online': 'Offline'}</span> </h2>\n                                                <p>Joined: {moment(member.created).fromNow()}</p>\n                                            </div>\n\n                                        </div>\n                                    )\n\n                                })}\n\n                            </div>\n                        </div> : null}\n\n\n                    </div>\n                </div>\n            </div>\n        )\n    }\n}"
  },
  {
    "path": "app/src/components/search-user.js",
    "content": "import React, {Component} from 'react'\nimport _ from 'lodash'\n\nexport default class SearchUser extends Component{\n\n\n\tconstructor(props){\n\t\tsuper(props);\n\n\n\t\tthis.handleOnClick = this.handleOnClick.bind(this);\n\n\n\t}\n\n\n\thandleOnClick(user){\n\n\n\t\tif(this.props.onSelect){\n\t\t\tthis.props.onSelect(user);\n\t\t}\n\t}\n\trender(){\n\n\t\tconst {store} = this.props;\n\n\t\t\n\t\tconst users = store.getSearchUsers();\n\n\n\n\n\t\treturn <div className=\"search-user\">\n\n\t\t\t<div className=\"user-list\">\n\n\t\t\t{users.map((user, index) => {\n\n\t\t\t\treturn (<div onClick={() => this.handleOnClick(user)} key={index} className=\"user\">\n\t\t\t\t\t<img src={_.get(user, 'avatar')} alt=\"...\" />\n\t\t\t\t\t<h2>{_.get(user, 'name')}</h2>\n\t\t\t\t</div>)\n\n\t\t\t})}\n\t\t\t\t\n\t\t\t\t\n\n\t\t\t</div>\n\t\t</div>\n\t}\n}"
  },
  {
    "path": "app/src/components/user-bar.js",
    "content": "import React, {Component} from 'react'\nimport _ from 'lodash'\nimport avatar from '../images/avatar.png'\nimport UserForm from './user-form'\nimport UserMenu from './user-menu'\n\n\nexport default class UserBar extends Component {\n\n    constructor(props) {\n        super(props);\n\n        this.state = {\n            showUserForm: false,\n            showUserMenu: false,\n        }\n\n\n    }\n\n    render() {\n\n        const {store} = this.props;\n\n        const me = store.getCurrentUser();\n        const profilePicture = _.get(me, 'avatar');\n        const isConnected = store.isConnected();\n\n        return (\n            <div className=\"user-bar\">\n                {me && !isConnected ? <div className=\"app-warning-state\">Reconnecting... </div> : null}\n                {!me ? <button onClick={() => {\n\n                    this.setState({\n                        showUserForm: true,\n                    })\n\n                }} type=\"button\" className=\"login-btn\">Sign In</button> : null}\n                <div className=\"profile-name\">{_.get(me, 'name')}</div>\n                <div className=\"profile-image\" onClick={() => {\n\n                    this.setState({\n                        showUserMenu: true,\n                    })\n\n                }}><img src={profilePicture ? profilePicture : avatar} alt=\"\"/></div>\n\n                {!me && this.state.showUserForm ? <UserForm onClose={(msg) => {\n\n\n                    this.setState({\n                        showUserForm: false,\n                    })\n\n                }} store={store}/> : null}\n\n\n                {this.state.showUserMenu ? <UserMenu\n                    store={store}\n                    onClose={() => {\n\n                        this.setState({\n                            showUserMenu: false,\n                        })\n                    }}\n\n                /> : null}\n\n            </div>\n        );\n    }\n}"
  },
  {
    "path": "app/src/components/user-form.js",
    "content": "import React, {Component} from 'react'\nimport _ from 'lodash'\nimport classNames from 'classnames'\n\n\nexport default class UserForm extends Component {\n\n    constructor(props) {\n\n        super(props);\n\n\n        this.state = {\n            message: null,\n            isLogin: true,\n            user: {\n                name: '',\n                email: '',\n                password: ''\n            }\n        }\n\n        this.onSubmit = this.onSubmit.bind(this);\n        this.onTextFieldChange = this.onTextFieldChange.bind(this)\n\n        this.onClickOutside = this.onClickOutside.bind(this);\n    }\n\n    onClickOutside(event) {\n\n        if (this.ref && !this.ref.contains(event.target)) {\n\n\n            if (this.props.onClose) {\n                this.props.onClose();\n            }\n\n        }\n    }\n\n    componentDidMount() {\n\n        window.addEventListener('mousedown', this.onClickOutside);\n\n    }\n\n    componentWillUnmount() {\n\n        window.removeEventListener('mousedown', this.onClickOutside);\n\n    }\n\n    onSubmit(event) {\n        const {user, isLogin} = this.state;\n        const {store} = this.props;\n\n        event.preventDefault();\n\n        this.setState({\n            message: null,\n        }, () => {\n\n\n            if(isLogin){\n                store.login(user.email, user.password).then((user) => {\n\n\n                    if (this.props.onClose) {\n                        this.props.onClose();\n                    }\n\n\n                }).catch((err) => {\n\n                    console.log(\"err\", err);\n\n                    this.setState({\n                        message: {\n                            body: err,\n                            type: 'error',\n                        }\n                    })\n                });\n            }else{\n\n                store.register(user).then((_)=> {\n\n                    this.setState({\n                        message: {\n                            body: 'User created.',\n                            type: 'success'\n                        }\n                    }, () => {\n\n                        // now login this user\n\n                        store.login(user.email, user.password).then(() => {\n\n                            if (this.props.onClose) {\n                                this.props.onClose();\n                            }\n                        })\n                    })\n                })\n            }\n\n\n        })\n\n\n    }\n\n    onTextFieldChange(event) {\n\n        let {user} = this.state;\n\n\n        const field = event.target.name;\n\n        user[field] = event.target.value;\n\n        this.setState({\n            user: user\n        });\n\n\n    }\n\n    render() {\n\n        const {user, message, isLogin} = this.state;\n\n        return (\n\n            <div className=\"user-form\" ref={(ref) => this.ref = ref}>\n\n                <form onSubmit={this.onSubmit} method=\"post\">\n                    {message ?\n                        <p className={classNames('app-message', _.get(message, 'type'))}>{_.get(message, 'body')}</p> : null}\n\n                    {!isLogin ?  <div className=\"form-item\">\n                        <label>Name</label>\n                        <input placeholder={'Full name'} onChange={this.onTextFieldChange} type={'text'} value={_.get(user, 'name', '')} name={\"name\"} />\n                    </div> : null }\n\n                    <div className=\"form-item\">\n                        <label>Email</label>\n                        <input value={_.get(user, 'email')} onChange={this.onTextFieldChange} type=\"email\"\n                               placeholder=\"Email addresss...\" name=\"email\"/>\n                    </div>\n\n                    <div className=\"form-item\">\n                        <label>Password</label>\n                        <input value={_.get(user, 'password')} onChange={this.onTextFieldChange} type=\"password\"\n                               placeholder=\"Password\" name=\"password\"/>\n                    </div>\n\n                    <div className=\"form-actions\">\n                        {isLogin ? <button onClick={() => {\n                            this.setState({\n                                isLogin: false,\n                            })\n\n                        }} type=\"button\">Create an account?\n                        </button> : null}\n\n                        <button className=\"primary\" type=\"submit\">{isLogin ? 'Sign In' : 'Create new account'}</button>\n                    </div>\n                </form>\n            </div>\n\n        );\n    }\n}"
  },
  {
    "path": "app/src/components/user-menu.js",
    "content": "import React,{Component} from 'react'\n\n\nexport default class UserMenu extends Component{\n\n\tconstructor(props){\n\t\tsuper(props);\n\n\n\n\t\tthis.onClickOutside = this.onClickOutside.bind(this);\n\n\n\n\t}\n\n\n\tonClickOutside(event){\n\n\t\tif(this.ref && !this.ref.contains(event.target)){\n\n\t\n\t\t\tif(this.props.onClose){\n\t\t\t\tthis.props.onClose();\n\t\t\t}\n\n\t\t}\n\t}\n\n\tcomponentDidMount(){\n\n\t\twindow.addEventListener('mousedown', this.onClickOutside);\n\n\t}\n\tcomponentWillUnmount(){\n\n\t\twindow.removeEventListener('mousedown', this.onClickOutside);\n\n\t}\n\n\n\n\trender(){\n\n\t\tconst {store} = this.props;\n\n\t\tconst user = store.getCurrentUser();\n\n\t\treturn <div className=\"user-menu\" ref={(ref) => this.ref = ref}>\n\t\t\t{user ? <div>\n\n                <h2>My menu</h2>\n                <ul className=\"menu\">\n                    <li><button onClick={() => {\n                        if(this.props.onClose){\n                            this.props.onClose();\n                        }\n\n                        store.signOut();\n\n                    }} type=\"button\">Sign Out</button></li>\n                </ul>\n\n\t\t\t\t</div> : null }\n\n\t\t</div>\n\t}\n}"
  },
  {
    "path": "app/src/config.js",
    "content": "export const production = false; // set it to true when deploy to the server\n\nconst 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\nexport const websocketUrl = `ws://${domain}`\nexport const apiUrl = `http://${domain}`"
  },
  {
    "path": "app/src/css/_font.scss",
    "content": "@charset \"UTF-8\";\n\n@font-face {\n  font-family: \"chatapp\";\n  src:url(\"./fonts/chatapp.eot\");\n  src:url(\"./fonts/chatapp.eot?#iefix\") format(\"embedded-opentype\"),\n    url(\"./fonts/chatapp.woff\") format(\"woff\"),\n    url(\"./fonts/chatapp.ttf\") format(\"truetype\"),\n    url(\"./fonts/chatapp.svg#chatapp\") format(\"svg\");\n  font-weight: normal;\n  font-style: normal;\n\n}\n\n[data-icon]:before {\n  font-family: \"chatapp\" !important;\n  content: attr(data-icon);\n  font-style: normal !important;\n  font-weight: normal !important;\n  font-variant: normal !important;\n  text-transform: none !important;\n  speak: none;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n[class^=\"icon-\"]:before,\n[class*=\" icon-\"]:before {\n  font-family: \"chatapp\" !important;\n  font-style: normal !important;\n  font-weight: normal !important;\n  font-variant: normal !important;\n  text-transform: none !important;\n  speak: none;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale;\n}\n\n.icon-edit-modify-streamline:before {\n  content: \"\\61\";\n}\n.icon-settings-streamline-1:before {\n  content: \"\\63\";\n}\n.icon-paperplane:before {\n  content: \"\\62\";\n}\n"
  },
  {
    "path": "app/src/css/_variable.scss",
    "content": "$header-height: 50px;\n$left-sidebar-width: 200px;\n$right-sidebar-width: 300px;\n$border-color: rgba(0, 0, 0, 0.05);\n$primary-color: #2ecc71;\n$danger-color: #e74c3c;\n$body-color: #2c3e50;"
  },
  {
    "path": "app/src/css/app.css",
    "content": "@import \"https://fonts.googleapis.com/css?family=Open+Sans:400,600\";\n@font-face {\n  font-family: \"chatapp\";\n  src: url(\"./fonts/chatapp.eot\");\n  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\");\n  font-weight: normal;\n  font-style: normal; }\n[data-icon]:before {\n  font-family: \"chatapp\" !important;\n  content: attr(data-icon);\n  font-style: normal !important;\n  font-weight: normal !important;\n  font-variant: normal !important;\n  text-transform: none !important;\n  speak: none;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale; }\n\n[class^=\"icon-\"]:before,\n[class*=\" icon-\"]:before {\n  font-family: \"chatapp\" !important;\n  font-style: normal !important;\n  font-weight: normal !important;\n  font-variant: normal !important;\n  text-transform: none !important;\n  speak: none;\n  line-height: 1;\n  -webkit-font-smoothing: antialiased;\n  -moz-osx-font-smoothing: grayscale; }\n\n.icon-edit-modify-streamline:before {\n  content: \"\\61\"; }\n\n.icon-settings-streamline-1:before {\n  content: \"\\63\"; }\n\n.icon-paperplane:before {\n  content: \"\\62\"; }\n\nbody, html {\n  margin: 0;\n  padding: 0;\n  height: 100%; }\n\nbody {\n  color: #2c3e50;\n  font-size: 13px;\n  font-family: 'Open Sans', sans-serif; }\n\n* {\n  box-sizing: border-box;\n  padding: 0;\n  margin: 0; }\n\n.app-messenger {\n  display: flex;\n  flex-direction: column; }\n  .app-messenger .header {\n    height: 50px;\n    display: flex;\n    flex-direction: row;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.05); }\n    .app-messenger .header .left {\n      width: 200px;\n      position: relative; }\n      .app-messenger .header .left .left-action {\n        position: absolute;\n        left: 8px;\n        top: 0; }\n      .app-messenger .header .left .right-action {\n        position: absolute;\n        right: 8px;\n        top: 0; }\n      .app-messenger .header .left h2 {\n        line-height: 50px;\n        font-size: 14px;\n        font-weight: 600;\n        display: block;\n        text-align: center; }\n      .app-messenger .header .left button {\n        background: none;\n        line-height: 50px;\n        border: 0 none;\n        font-size: 20px;\n        cursor: pointer; }\n    .app-messenger .header .content {\n      flex-grow: 1; }\n      .app-messenger .header .content h2 {\n        line-height: 50px;\n        text-align: center; }\n    .app-messenger .header .right {\n      width: 300px; }\n      .app-messenger .header .right .user-bar {\n        line-height: 50px;\n        display: flex;\n        justify-content: flex-end;\n        padding: 0 10px; }\n        .app-messenger .header .right .user-bar .profile-name {\n          padding-right: 10px; }\n        .app-messenger .header .right .user-bar .profile-image {\n          line-height: 50px; }\n          .app-messenger .header .right .user-bar .profile-image img {\n            width: 30px;\n            height: 30px;\n            border-radius: 50%;\n            margin: 10px 0 0 0; }\n  .app-messenger .main {\n    height: 100%;\n    display: flex;\n    overflow: hidden; }\n    .app-messenger .main .sidebar-left {\n      width: 200px;\n      border-right: 1px solid rgba(0, 0, 0, 0.05); }\n    .app-messenger .main .sidebar-right {\n      border-left: 1px solid rgba(0, 0, 0, 0.05);\n      width: 300px; }\n      .app-messenger .main .sidebar-right .title {\n        padding: 10px; }\n    .app-messenger .main .content {\n      flex-grow: 1;\n      overflow: hidden;\n      display: flex;\n      flex-direction: column; }\n      .app-messenger .main .content .messages {\n        flex-grow: 1; }\n      .app-messenger .main .content .messenger-input {\n        border-top: 1px solid rgba(0, 0, 0, 0.05);\n        height: 50px;\n        display: flex;\n        flex-direction: row; }\n        .app-messenger .main .content .messenger-input .text-input {\n          flex-grow: 1; }\n          .app-messenger .main .content .messenger-input .text-input textarea {\n            border: 0 none;\n            width: 100%;\n            height: 100%;\n            padding: 8px 15px; }\n        .app-messenger .main .content .messenger-input .actions button.send {\n          background: #2ecc71;\n          color: #FFF;\n          border: 0 none;\n          padding: 7px 15px;\n          line-height: 50px; }\n\n.messages {\n  display: flex;\n  flex-direction: column;\n  overflow-y: auto;\n  height: 100%; }\n  .messages .message {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-start;\n    margin: 15px; }\n    .messages .message .message-user-image img {\n      width: 20px;\n      height: 20px;\n      border-radius: 50%; }\n    .messages .message .message-body {\n      padding-left: 10px; }\n      .messages .message .message-body .message-text {\n        background: rgba(0, 0, 0, 0.05);\n        padding: 10px;\n        border-radius: 10px; }\n    .messages .message.me {\n      justify-content: flex-end; }\n      .messages .message.me .message-body .message-text {\n        background: #2ecc71;\n        color: #FFF; }\n\n.chanels {\n  overflow-y: auto;\n  height: 100%; }\n  .chanels .chanel {\n    cursor: pointer;\n    display: flex;\n    border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n    padding: 8px; }\n    .chanels .chanel .user-image {\n      width: 30px; }\n      .chanels .chanel .user-image img {\n        max-width: 100%; }\n      .chanels .chanel .user-image .channel-avatars {\n        overflow: hidden;\n        width: 30px;\n        height: 30px;\n        border-radius: 50%;\n        background-color: #ccc;\n        position: relative; }\n        .chanels .chanel .user-image .channel-avatars.channel-avatars-1 img {\n          width: 100%;\n          height: 100%;\n          border-radius: 50%; }\n        .chanels .chanel .user-image .channel-avatars.channel-avatars-2 img {\n          width: 50%;\n          height: 100%;\n          position: absolute;\n          right: 0;\n          top: 0; }\n          .chanels .chanel .user-image .channel-avatars.channel-avatars-2 img:first-child {\n            left: 0;\n            top: 0; }\n        .chanels .chanel .user-image .channel-avatars.channel-avatars-3 img {\n          position: absolute;\n          width: 50%;\n          height: 50%;\n          right: 0;\n          top: 0; }\n          .chanels .chanel .user-image .channel-avatars.channel-avatars-3 img:first-child {\n            left: 0;\n            top: 0;\n            width: 50%;\n            height: 100%; }\n          .chanels .chanel .user-image .channel-avatars.channel-avatars-3 img:last-child {\n            bottom: 0;\n            right: 0;\n            top: 15px;\n            width: 50%;\n            height: 50%; }\n        .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img {\n          position: absolute;\n          width: 50%;\n          height: 50%;\n          right: 0;\n          top: 0; }\n          .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:first-child {\n            left: 0;\n            top: 0;\n            width: 50%;\n            height: 100%; }\n          .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:nth-child(3n) {\n            bottom: 0;\n            right: 0;\n            top: 15px;\n            width: 50%;\n            height: 50%; }\n          .chanels .chanel .user-image .channel-avatars.channel-avatars-4 img:last-child {\n            left: 0;\n            bottom: 0;\n            top: 15px; }\n    .chanels .chanel .chanel-info {\n      flex-grow: 1;\n      padding-left: 8px;\n      padding-right: 8px;\n      overflow: hidden; }\n      .chanels .chanel .chanel-info h2 {\n        font-size: 13px;\n        font-weight: 400;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        overflow: hidden; }\n      .chanels .chanel .chanel-info p {\n        font-size: 12px;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        overflow: hidden; }\n    .chanels .chanel.active {\n      background: rgba(0, 0, 0, 0.05); }\n    .chanels .chanel.notify .chanel-info p {\n      color: #2ecc71; }\n\n.members .member {\n  display: flex;\n  border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n  padding: 8px; }\n  .members .member .user-image {\n    width: 30px;\n    position: relative; }\n    .members .member .user-image img {\n      width: 30px;\n      height: 30px;\n      border-radius: 50%; }\n    .members .member .user-image .user-status {\n      width: 8px;\n      height: 8px;\n      display: block;\n      position: absolute;\n      right: 0;\n      bottom: 10px;\n      border: 1px solid #FFFFFF;\n      background: #cccccc;\n      -webkit-border-radius: 50%;\n      -moz-border-radius: 50%;\n      border-radius: 50%; }\n      .members .member .user-image .user-status.online {\n        background: #2ecc71; }\n  .members .member .member-info {\n    padding-left: 8px;\n    flex-grow: 1; }\n    .members .member .member-info h2 {\n      font-size: 14px; }\n    .members .member .member-info p {\n      font-size: 12px; }\n\nh2.title {\n  font-size: 16px;\n  font-weight: 600;\n  color: rgba(0, 0, 0, 0.8); }\n\n.toolbar {\n  height: 50px;\n  display: flex;\n  flex-direction: row;\n  position: relative; }\n  .toolbar span {\n    line-height: 20px;\n    height: 30px;\n    background: #2ecc71;\n    color: #FFF;\n    cursor: pointer;\n    display: block;\n    border-radius: 3px;\n    margin: 10px 5px 0 0;\n    padding: 5px 8px; }\n  .toolbar label {\n    line-height: 50px; }\n  .toolbar input {\n    height: 30px;\n    line-height: 30px;\n    margin-top: 10px;\n    border: 0 none; }\n  .toolbar .search-user {\n    min-width: 180px;\n    position: absolute;\n    left: 0;\n    top: 50px;\n    z-index: 1;\n    border: 1px solid rgba(0, 0, 0, 0.05);\n    border-top: 0 none; }\n    .toolbar .search-user .user-list {\n      display: flex;\n      flex-direction: column; }\n      .toolbar .search-user .user-list .user {\n        display: flex;\n        flex-direction: row;\n        padding: 5px;\n        border-bottom: 1px solid rgba(0, 0, 0, 0.05);\n        cursor: pointer; }\n        .toolbar .search-user .user-list .user img {\n          width: 30px;\n          height: 30px;\n          border-radius: 50%;\n          margin-top: 10px; }\n        .toolbar .search-user .user-list .user h2 {\n          padding-left: 8px;\n          flex-grow: 1;\n          font-size: 14px; }\n        .toolbar .search-user .user-list .user:last-child {\n          border-bottom: 0 none; }\n        .toolbar .search-user .user-list .user:hover {\n          background: rgba(0, 0, 0, 0.02); }\n\n.user-bar {\n  position: relative; }\n  .user-bar button.login-btn {\n    height: 50px;\n    border: 0 none;\n    background: none;\n    color: #2ecc71;\n    font-weight: 600;\n    font-size: 14px; }\n  .user-bar .user-form {\n    background: #FFF;\n    box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);\n    position: absolute;\n    top: 50px;\n    right: 0;\n    border: 1px solid rgba(0, 0, 0, 0.05);\n    border-top: 0 none;\n    padding: 10px; }\n    .user-bar .user-form .form-item label {\n      line-height: 30px;\n      min-width: 75px;\n      text-align: right;\n      margin-right: 8px; }\n    .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\"] {\n      height: 30px;\n      line-height: 30px; }\n    .user-bar .user-form .form-actions {\n      display: flex;\n      flex-direction: row;\n      justify-content: flex-end; }\n  .user-bar .user-menu {\n    background: #FFF;\n    box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);\n    min-width: 200px;\n    position: absolute;\n    right: 0;\n    top: 50px;\n    border: 1px solid rgba(0, 0, 0, 0.05);\n    border-top: 0 none; }\n    .user-bar .user-menu ul {\n      padding: 0;\n      margin: 0;\n      list-style: none; }\n      .user-bar .user-menu ul li {\n        border-top: 1px solid rgba(0, 0, 0, 0.05);\n        padding: 8px; }\n        .user-bar .user-menu ul li button {\n          background: none;\n          border: 0 none;\n          display: block;\n          cursor: pointer;\n          text-align: center;\n          width: 100%; }\n        .user-bar .user-menu ul li:hover {\n          background: rgba(0, 0, 0, 0.09); }\n    .user-bar .user-menu h2 {\n      font-size: 14px;\n      font-weight: 600;\n      margin: 0;\n      display: block;\n      text-align: center; }\n\n.form-item {\n  display: flex;\n  margin-bottom: 10px; }\n  .form-item label {\n    font-weight: 600; }\n  .form-item input[type=\"email\"], .form-item input[type=\"password\"], .form-item input[type=\"text\"] {\n    border: 1px solid rgba(0, 0, 0, 0.05);\n    padding: 3px 8px; }\n\n.form-actions button {\n  border: 0 none;\n  padding: 7px 15px;\n  text-align: center; }\n  .form-actions button.primary {\n    background: #2ecc71;\n    color: #FFF; }\n\n.app-message {\n  line-height: 1.5em;\n  padding: 10px;\n  font-size: 12px;\n  text-align: center;\n  border: 1px solid #2ecc71;\n  border-radius: 5px;\n  margin: 0 0 10px 0; }\n  .app-message.error {\n    background: #e74c3c;\n    color: #FFF;\n    border-color: #e74c3c; }\n\n.user-status {\n  font-size: 10px;\n  color: #2c3e50; }\n  .user-status.online {\n    color: #2ecc71; }\n\n.app-warning-state {\n  font-size: 10px;\n  padding: 0 10px;\n  color: #e74c3c; }\n\n/*# sourceMappingURL=app.css.map */\n"
  },
  {
    "path": "app/src/css/app.scss",
    "content": "@import \"font\";\n@import \"https://fonts.googleapis.com/css?family=Open+Sans:400,600\";\n@import 'variable';\n\nbody, html {\n  margin: 0;\n  padding: 0;\n  height: 100%;\n}\n\nbody {\n  color: $body-color;\n  font-size: 13px;\n  font-family: 'Open Sans', sans-serif;\n}\n\n* {\n  box-sizing: border-box;\n  padding: 0;\n  margin: 0;\n}\n\n.app-messenger {\n  display: flex;\n  flex-direction: column;\n  .header {\n    height: $header-height;\n    display: flex;\n    flex-direction: row;\n    border-bottom: 1px solid $border-color;\n    .left {\n      width: $left-sidebar-width;\n      position: relative;\n      .left-action {\n        position: absolute;\n        left: 8px;\n        top: 0;\n      }\n      .right-action {\n        position: absolute;\n        right: 8px;\n        top: 0;\n      }\n      h2 {\n        line-height: $header-height;\n        font-size: 14px;\n        font-weight: 600;\n        display: block;\n        text-align: center;\n      }\n      button {\n        background: none;\n        line-height: $header-height;\n        border: 0 none;\n        font-size: 20px;\n        cursor: pointer;\n\n      }\n    }\n    .content {\n      flex-grow: 1;\n\n      h2 {\n        line-height: $header-height;\n        text-align: center;\n      }\n    }\n    .right {\n      width: $right-sidebar-width;\n      .user-bar {\n        line-height: $header-height;\n        display: flex;\n        justify-content: flex-end;\n        padding: 0 10px;\n        .profile-name {\n          padding-right: 10px;\n        }\n        .profile-image {\n          line-height: $header-height;\n          img {\n            width: 30px;\n            height: 30px;\n            border-radius: 50%;\n            margin: 10px 0 0 0;\n          }\n        }\n      }\n    }\n\n  }\n  .main {\n    height: 100%;\n    display: flex;\n    overflow: hidden;\n    .sidebar-left {\n\n      width: $left-sidebar-width;\n      border-right: 1px solid $border-color;\n\n    }\n    .sidebar-right {\n      border-left: 1px solid $border-color;\n      width: $right-sidebar-width;\n      .title {\n        padding: 10px;\n      }\n    }\n    .content {\n      flex-grow: 1;\n      overflow: hidden;\n      display: flex;\n      flex-direction: column;\n      .messages {\n        flex-grow: 1;\n      }\n      .messenger-input {\n        border-top: 1px solid $border-color;\n        height: 50px;\n        display: flex;\n        flex-direction: row;\n        .text-input {\n          flex-grow: 1;\n          textarea {\n            border: 0 none;\n            width: 100%;\n            height: 100%;\n            padding: 8px 15px;\n\n          }\n        }\n        .actions {\n\n          button.send {\n            background: $primary-color;\n            color: #FFF;\n            border: 0 none;\n            padding: 7px 15px;\n            line-height: 50px;\n          }\n        }\n      }\n\n    }\n\n  }\n}\n\n.messages {\n  display: flex;\n  flex-direction: column;\n  overflow-y: auto;\n  height: 100%;\n  .message {\n    display: flex;\n    flex-direction: row;\n    justify-content: flex-start;\n    margin: 15px;\n    .message-user-image {\n      img {\n        width: 20px;\n        height: 20px;\n        border-radius: 50%;\n      }\n    }\n    .message-body {\n      padding-left: 10px;\n      .message-author {\n\n      }\n      .message-text {\n        background: rgba(0, 0, 0, 0.05);\n        padding: 10px;\n        border-radius: 10px;\n      }\n    }\n\n    &.me {\n      justify-content: flex-end;\n      .message-body {\n        .message-text {\n          background: $primary-color;\n          color: #FFF;\n        }\n      }\n    }\n  }\n}\n\n.chanels {\n\n  overflow-y: auto;\n  height: 100%;\n  .chanel {\n    cursor: pointer;\n    display: flex;\n    border-bottom: 1px solid $border-color;\n    padding: 8px;\n    .user-image {\n      width: 30px;\n      img {\n        max-width: 100%;\n      }\n      .channel-avatars {\n        overflow: hidden;\n        width: 30px;\n        height: 30px;\n        border-radius: 50%;\n        background-color: #ccc;\n        position: relative;\n\n        &.channel-avatars-1 {\n          img {\n            width: 100%;\n            height: 100%;\n            border-radius: 50%;\n          }\n        }\n        &.channel-avatars-2 {\n\n          img {\n            width: 50%;\n            height: 100%;\n            position: absolute;\n            right: 0;\n            top: 0;\n            &:first-child {\n              left: 0;\n              top: 0;\n            }\n          }\n        }\n        &.channel-avatars-3 {\n\n          img {\n            position: absolute;\n            width: 50%;\n            height: 50%;\n            right: 0;\n            top: 0;\n            &:first-child {\n              left: 0;\n              top: 0;\n              width: 50%;\n              height: 100%;\n            }\n            &:last-child {\n              bottom: 0;\n              right: 0;\n              top: 15px;\n              width: 50%;\n              height: 50%;\n            }\n\n          }\n        }\n        &.channel-avatars-4 {\n          img {\n            position: absolute;\n            width: 50%;\n            height: 50%;\n            right: 0;\n            top: 0;\n            &:first-child {\n              left: 0;\n              top: 0;\n              width: 50%;\n              height: 100%;\n            }\n            &:nth-child(3n) {\n              bottom: 0;\n              right: 0;\n              top: 15px;\n              width: 50%;\n              height: 50%;\n            }\n            &:last-child {\n\n              left: 0;\n              bottom: 0;\n              top: 15px;\n            }\n\n          }\n        }\n      }\n    }\n    .chanel-info {\n      flex-grow: 1;\n      padding-left: 8px;\n      padding-right: 8px;\n      overflow: hidden;\n      h2 {\n        font-size: 13px;\n        font-weight: 400;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        overflow: hidden;\n      }\n      p {\n        font-size: 12px;\n        white-space: nowrap;\n        text-overflow: ellipsis;\n        overflow: hidden;\n      }\n    }\n    &.active {\n      background: rgba(0, 0, 0, 0.05);\n    }\n    &.notify {\n      .chanel-info {\n        p {\n          color: $primary-color;\n        }\n      }\n    }\n  }\n}\n\n.members {\n\n  .member {\n    display: flex;\n    border-bottom: 1px solid $border-color;\n    padding: 8px;\n    .user-image {\n      width: 30px;\n      position: relative;\n      img {\n        width: 30px;\n        height: 30px;\n        border-radius: 50%;\n      }\n      .user-status{\n        width: 8px;\n        height: 8px;\n        display: block;\n        position: absolute;\n        right: 0;\n        bottom: 10px;\n        border: 1px solid #FFFFFF;\n        background: #cccccc;\n        -webkit-border-radius: 50%;\n        -moz-border-radius: 50%;\n        border-radius: 50%;\n        &.online{\n          background: $primary-color;\n        }\n      }\n    }\n    .member-info {\n      padding-left: 8px;\n      flex-grow: 1;\n      h2 {\n        font-size: 14px;\n      }\n      p {\n\n        font-size: 12px;\n      }\n    }\n  }\n}\n\nh2.title {\n  font-size: 16px;\n  font-weight: 600;\n  color: rgba(0, 0, 0, 0.8);\n}\n\n.toolbar {\n  height: $header-height;\n  display: flex;\n  flex-direction: row;\n  position: relative;\n  span {\n    line-height: 20px;\n    height: 30px;\n    background: $primary-color;\n    color: #FFF;\n    cursor: pointer;\n    display: block;\n    border-radius: 3px;\n    margin: 10px 5px 0 0;\n    padding: 5px 8px;\n  }\n  label {\n    line-height: $header-height;\n  }\n  input {\n    height: 30px;\n    line-height: 30px;\n    margin-top: 10px;\n    border: 0 none;\n  }\n  .search-user {\n    min-width: 180px;\n    position: absolute;\n    left: 0;\n    top: $header-height;\n    z-index: 1;\n    border: 1px solid $border-color;\n    border-top: 0 none;\n    .user-list {\n      display: flex;\n      flex-direction: column;\n      .user {\n        display: flex;\n        flex-direction: row;\n        padding: 5px;\n        border-bottom: 1px solid $border-color;\n        cursor: pointer;\n        img {\n\n          width: 30px;\n          height: 30px;\n          border-radius: 50%;\n          margin-top: 10px;\n        }\n        h2 {\n          padding-left: 8px;\n          flex-grow: 1;\n          font-size: 14px;\n        }\n        &:last-child {\n          border-bottom: 0 none;\n        }\n        &:hover {\n          background: rgba(0, 0, 0, 0.02);\n        }\n      }\n    }\n  }\n}\n\n.user-bar {\n  position: relative;\n  button.login-btn {\n    height: $header-height;\n    border: 0 none;\n    background: none;\n    color: $primary-color;\n    font-weight: 600;\n    font-size: 14px;\n  }\n\n  .user-form {\n    background: #FFF;\n    box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);\n    position: absolute;\n    top: $header-height;\n    right: 0;\n    border: 1px solid $border-color;\n    border-top: 0 none;\n    padding: 10px;\n    .form-item {\n      label {\n        line-height: 30px;\n        min-width: 75px;\n        text-align: right;\n        margin-right: 8px;\n      }\n      input[type=\"email\"], input[type=\"password\"], input[type=\"text\"] {\n\n        height: 30px;\n        line-height: 30px;\n\n      }\n    }\n    .form-actions {\n      display: flex;\n      flex-direction: row;\n      justify-content: flex-end;\n    }\n  }\n  .user-menu {\n    background: #FFF;\n    box-shadow: -1px 1px 1px rgba(0, 0, 0, 0.09);\n    min-width: 200px;\n    position: absolute;\n    right: 0;\n    top: $header-height;\n    border: 1px solid $border-color;\n    border-top: 0 none;\n    ul {\n      padding: 0;\n      margin: 0;\n      list-style: none;\n      li {\n        border-top: 1px solid $border-color;\n        padding: 8px;\n        button {\n          background: none;\n          border: 0 none;\n          display: block;\n          cursor: pointer;\n          text-align: center;\n          width: 100%;\n        }\n        &:hover {\n          background: rgba(0, 0, 0, 0.09);\n\n        }\n      }\n    }\n    h2 {\n      font-size: 14px;\n      font-weight: 600;\n      margin: 0;\n      display: block;\n      text-align: center;\n    }\n  }\n}\n\n.form-item {\n  display: flex;\n  margin-bottom: 10px;\n  label {\n    font-weight: 600;\n  }\n  input[type=\"email\"], input[type=\"password\"], input[type=\"text\"] {\n    border: 1px solid $border-color;\n    padding: 3px 8px;\n  }\n}\n\n.form-actions {\n  button {\n    border: 0 none;\n    padding: 7px 15px;\n    text-align: center;\n    &.primary {\n      background: $primary-color;\n      color: #FFF;\n    }\n  }\n}\n\n.app-message {\n  line-height: 1.5em;\n  padding: 10px;\n  font-size: 12px;\n  text-align: center;\n  border: 1px solid $primary-color;\n  border-radius: 5px;\n  margin: 0 0 10px 0;\n  &.error {\n    background: $danger-color;\n    color: #FFF;\n    border-color: $danger-color;\n  }\n}\n.user-status{\n  font-size: 10px;\n  color: $body-color;\n  &.online{\n    color: $primary-color;\n  }\n}\n.app-warning-state{\n  font-size: 10px;\n  padding: 0 10px;\n  color: $danger-color;\n}\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "app/src/helpers/index.js",
    "content": ""
  },
  {
    "path": "app/src/helpers/objectid.js",
    "content": "/**\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 machine id here, but\n * that would mean an asyc call to gethostname, so we don't bother.\n * @ignore\n */\nvar MACHINE_ID = parseInt(Math.random() * 0xffffff, 10);\n\n// Regular expression that checks for hex value\nvar checkForHexRegExp = new RegExp('^[0-9a-fA-F]{24}$');\n\n// Check if buffer exists\ntry {\n    if (Buffer && Buffer.from) var hasBufferType = true;\n} catch (err) {\n    hasBufferType = false;\n}\n\n/**\n * Create a new ObjectID instance\n *\n * @class\n * @param {(string|number)} id Can be a 24 byte hex string, 12 byte binary string or a Number.\n * @property {number} generationTime The generation time of this ObjectId instance\n * @return {ObjectID} instance of ObjectID.\n */\nvar ObjectID = function ObjectID(id) {\n    // Duck-typing to support ObjectId from different npm packages\n    if (id instanceof ObjectID) return id;\n    if (!(this instanceof ObjectID)) return new ObjectID(id);\n\n    this._bsontype = 'ObjectID';\n\n    // The most common usecase (blank id, new objectId instance)\n    if (id == null || typeof id === 'number') {\n        // Generate a new id\n        this.id = this.generate(id);\n        // If we are caching the hex string\n        if (ObjectID.cacheHexString) this.__id = this.toString('hex');\n        // Return the object\n        return;\n    }\n\n    // Check if the passed in id is valid\n    var valid = ObjectID.isValid(id);\n\n    // Throw an error if it's not a valid setup\n    if (!valid && id != null) {\n        throw new Error(\n            'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'\n        );\n    } else if (valid && typeof id === 'string' && id.length === 24 && hasBufferType) {\n        return new ObjectID(new Buffer(id, 'hex'));\n    } else if (valid && typeof id === 'string' && id.length === 24) {\n        return ObjectID.createFromHexString(id);\n    } else if (id != null && id.length === 12) {\n        // assume 12 byte string\n        this.id = id;\n    } else if (id != null && id.toHexString) {\n        // Duck-typing to support ObjectId from different npm packages\n        return id;\n    } else {\n        throw new Error(\n            'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'\n        );\n    }\n\n    if (ObjectID.cacheHexString) this.__id = this.toString('hex');\n};\n\n// Allow usage of ObjectId as well as ObjectID\n// var ObjectId = ObjectID;\n\n// Precomputed hex table enables speedy hex string conversion\nvar hexTable = [];\nfor (var i = 0; i < 256; i++) {\n    hexTable[i] = (i <= 15 ? '0' : '') + i.toString(16);\n}\n\n/**\n * Return the ObjectID id as a 24 byte hex string representation\n *\n * @method\n * @return {string} return the 24 byte hex string representation.\n */\nObjectID.prototype.toHexString = function() {\n    if (ObjectID.cacheHexString && this.__id) return this.__id;\n\n    var hexString = '';\n    if (!this.id || !this.id.length) {\n        throw new Error(\n            'invalid ObjectId, ObjectId.id must be either a string or a Buffer, but is [' +\n            JSON.stringify(this.id) +\n            ']'\n        );\n    }\n\n    if (this.id instanceof _Buffer) {\n        hexString = convertToHex(this.id);\n        if (ObjectID.cacheHexString) this.__id = hexString;\n        return hexString;\n    }\n\n    for (var i = 0; i < this.id.length; i++) {\n        hexString += hexTable[this.id.charCodeAt(i)];\n    }\n\n    if (ObjectID.cacheHexString) this.__id = hexString;\n    return hexString;\n};\n\n/**\n * Update the ObjectID index used in generating new ObjectID's on the driver\n *\n * @method\n * @return {number} returns next index value.\n * @ignore\n */\nObjectID.prototype.get_inc = function() {\n    return (ObjectID.index = (ObjectID.index + 1) % 0xffffff);\n};\n\n/**\n * Update the ObjectID index used in generating new ObjectID's on the driver\n *\n * @method\n * @return {number} returns next index value.\n * @ignore\n */\nObjectID.prototype.getInc = function() {\n    return this.get_inc();\n};\n\n/**\n * Generate a 12 byte id buffer used in ObjectID's\n *\n * @method\n * @param {number} [time] optional parameter allowing to pass in a second based timestamp.\n * @return {Buffer} return the 12 byte id buffer string.\n */\nObjectID.prototype.generate = function(time) {\n    if ('number' !== typeof time) {\n        time = ~~(Date.now() / 1000);\n    }\n\n    // Use pid\n    var pid =\n        (typeof process === 'undefined' || process.pid === 1\n            ? Math.floor(Math.random() * 100000)\n            : process.pid) % 0xffff;\n    var inc = this.get_inc();\n    // Buffer used\n    var buffer = new Buffer(12);\n    // Encode time\n    buffer[3] = time & 0xff;\n    buffer[2] = (time >> 8) & 0xff;\n    buffer[1] = (time >> 16) & 0xff;\n    buffer[0] = (time >> 24) & 0xff;\n    // Encode machine\n    buffer[6] = MACHINE_ID & 0xff;\n    buffer[5] = (MACHINE_ID >> 8) & 0xff;\n    buffer[4] = (MACHINE_ID >> 16) & 0xff;\n    // Encode pid\n    buffer[8] = pid & 0xff;\n    buffer[7] = (pid >> 8) & 0xff;\n    // Encode index\n    buffer[11] = inc & 0xff;\n    buffer[10] = (inc >> 8) & 0xff;\n    buffer[9] = (inc >> 16) & 0xff;\n    // Return the buffer\n    return buffer;\n};\n\n/**\n * Converts the id into a 24 byte hex string for printing\n *\n * @param {String} format The Buffer toString format parameter.\n * @return {String} return the 24 byte hex string representation.\n * @ignore\n */\nObjectID.prototype.toString = function(format) {\n    // Is the id a buffer then use the buffer toString method to return the format\n    if (this.id && this.id.copy) {\n        return this.id.toString(typeof format === 'string' ? format : 'hex');\n    }\n\n    // if(this.buffer )\n    return this.toHexString();\n};\n\n/**\n * Converts to a string representation of this Id.\n *\n * @return {String} return the 24 byte hex string representation.\n * @ignore\n */\nObjectID.prototype.inspect = ObjectID.prototype.toString;\n\n/**\n * Converts to its JSON representation.\n *\n * @return {String} return the 24 byte hex string representation.\n * @ignore\n */\nObjectID.prototype.toJSON = function() {\n    return this.toHexString();\n};\n\n/**\n * Compares the equality of this ObjectID with `otherID`.\n *\n * @method\n * @param {object} otherID ObjectID instance to compare against.\n * @return {boolean} the result of comparing two ObjectID's\n */\nObjectID.prototype.equals = function equals(otherId) {\n    // var id;\n\n    if (otherId instanceof ObjectID) {\n        return this.toString() === otherId.toString();\n    } else if (\n        typeof otherId === 'string' &&\n        ObjectID.isValid(otherId) &&\n        otherId.length === 12 &&\n        this.id instanceof _Buffer\n    ) {\n        return otherId === this.id.toString('binary');\n    } else if (typeof otherId === 'string' && ObjectID.isValid(otherId) && otherId.length === 24) {\n        return otherId.toLowerCase() === this.toHexString();\n    } else if (typeof otherId === 'string' && ObjectID.isValid(otherId) && otherId.length === 12) {\n        return otherId === this.id;\n    } else if (otherId != null && (otherId instanceof ObjectID || otherId.toHexString)) {\n        return otherId.toHexString() === this.toHexString();\n    } else {\n        return false;\n    }\n};\n\n/**\n * Returns the generation date (accurate up to the second) that this ID was generated.\n *\n * @method\n * @return {date} the generation date\n */\nObjectID.prototype.getTimestamp = function() {\n    var timestamp = new Date();\n    var time = this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);\n    timestamp.setTime(Math.floor(time) * 1000);\n    return timestamp;\n};\n\n/**\n * @ignore\n */\nObjectID.index = ~~(Math.random() * 0xffffff);\n\n/**\n * @ignore\n */\nObjectID.createPk = function createPk() {\n    return new ObjectID();\n};\n\n/**\n * Creates an ObjectID from a second based number, with the rest of the ObjectID zeroed out. Used for comparisons or sorting the ObjectID.\n *\n * @method\n * @param {number} time an integer number representing a number of seconds.\n * @return {ObjectID} return the created ObjectID\n */\nObjectID.createFromTime = function createFromTime(time) {\n    var buffer = new Buffer([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0]);\n    // Encode time into first 4 bytes\n    buffer[3] = time & 0xff;\n    buffer[2] = (time >> 8) & 0xff;\n    buffer[1] = (time >> 16) & 0xff;\n    buffer[0] = (time >> 24) & 0xff;\n    // Return the new objectId\n    return new ObjectID(buffer);\n};\n\n// Lookup tables\n//var encodeLookup = '0123456789abcdef'.split('');\nvar decodeLookup = [];\ni = 0;\nwhile (i < 10) decodeLookup[0x30 + i] = i++;\nwhile (i < 16) decodeLookup[0x41 - 10 + i] = decodeLookup[0x61 - 10 + i] = i++;\n\nvar _Buffer = Buffer;\nvar convertToHex = function(bytes) {\n    return bytes.toString('hex');\n};\n\n/**\n * Creates an ObjectID from a hex string representation of an ObjectID.\n *\n * @method\n * @param {string} hexString create a ObjectID from a passed in 24 byte hexstring.\n * @return {ObjectID} return the created ObjectID\n */\nObjectID.createFromHexString = function createFromHexString(string) {\n    // Throw an error if it's not a valid setup\n    if (typeof string === 'undefined' || (string != null && string.length !== 24)) {\n        throw new Error(\n            'Argument passed in must be a single String of 12 bytes or a string of 24 hex characters'\n        );\n    }\n\n    // Use Buffer.from method if available\n    if (hasBufferType) return new ObjectID(new Buffer(string, 'hex'));\n\n    // Calculate lengths\n    var array = new _Buffer(12);\n    var n = 0;\n    var i = 0;\n\n    while (i < 24) {\n        array[n++] = (decodeLookup[string.charCodeAt(i++)] << 4) | decodeLookup[string.charCodeAt(i++)];\n    }\n\n    return new ObjectID(array);\n};\n\n/**\n * Checks if a value is a valid bson ObjectId\n *\n * @method\n * @return {boolean} return true if the value is a valid bson ObjectId, return false otherwise.\n */\nObjectID.isValid = function isValid(id) {\n    if (id == null) return false;\n\n    if (typeof id === 'number') {\n        return true;\n    }\n\n    if (typeof id === 'string') {\n        return id.length === 12 || (id.length === 24 && checkForHexRegExp.test(id));\n    }\n\n    if (id instanceof ObjectID) {\n        return true;\n    }\n\n    if (id instanceof _Buffer) {\n        return true;\n    }\n\n    // Duck-Typing detection of ObjectId like objects\n    if (id.toHexString) {\n        return id.id.length === 12 || (id.id.length === 24 && checkForHexRegExp.test(id.id));\n    }\n\n    return false;\n};\n\n/**\n * @ignore\n */\nObject.defineProperty(ObjectID.prototype, 'generationTime', {\n    enumerable: true,\n    get: function() {\n        return this.id[3] | (this.id[2] << 8) | (this.id[1] << 16) | (this.id[0] << 24);\n    },\n    set: function(value) {\n        // Encode time into first 4 bytes\n        this.id[3] = value & 0xff;\n        this.id[2] = (value >> 8) & 0xff;\n        this.id[1] = (value >> 16) & 0xff;\n        this.id[0] = (value >> 24) & 0xff;\n    }\n});\n\n/**\n * Expose.\n */\nmodule.exports = ObjectID;\nmodule.exports.ObjectID = ObjectID;\nmodule.exports.ObjectId = ObjectID;"
  },
  {
    "path": "app/src/index.js",
    "content": "import React from 'react';\nimport ReactDOM from 'react-dom';\nimport App from './components/app';\nimport './css/app.css'\n\n\n//import registerServiceWorker from './registerServiceWorker';\n\nReactDOM.render(<App />, document.getElementById('root'));\n//registerServiceWorker();\n\n"
  },
  {
    "path": "app/src/realtime.js",
    "content": "import _ from 'lodash'\nimport {OrderedMap} from 'immutable'\nimport {websocketUrl} from './config'\n\nexport default class Realtime {\n\n\n    constructor(store) {\n\n        this.store = store;\n        this.ws = null;\n        this.isConnected = false;\n\n        this.connect();\n        this.reconnect();\n    }\n\n\n    reconnect(){\n\n        const store = this.store;\n\n        window.setInterval(()=>{\n\n            const user = store.getCurrentUser();\n            if(user && !this.isConnected){\n\n                console.log(\"try reconnecting...\");\n\n                this.connect();\n            }\n\n        }, 3000)\n    }\n\n    decodeMessage(msg) {\n\n        let message = {};\n\n        try {\n\n            message = JSON.parse(msg);\n\n        }\n        catch (err) {\n\n            console.log(err);\n        }\n\n        return message;\n    }\n\n    readMessage(msg) {\n\n        const store = this.store;\n        const currentUser = store.getCurrentUser();\n        const currentUserId = _.toString(_.get(currentUser, '_id'));\n        const message = this.decodeMessage(msg);\n\n        const action = _.get(message, 'action', '');\n\n        const payload = _.get(message, 'payload');\n\n        switch (action) {\n\n            case 'user_offline':\n\n                this.onUpdateUserStatus(payload, false);\n                break;\n            case 'user_online':\n\n                    const isOnline = true;\n                    this.onUpdateUserStatus(payload, isOnline);\n\n                break;\n            case 'message_added':\n\n                    const activeChannel = store.getActiveChannel();\n\n                    let notify = _.get(activeChannel, '_id') !== _.get(payload, 'channelId') && currentUserId !== _.get(payload, 'userId');\n                    this.onAddMessage(payload, notify);\n\n                break;\n\n            case 'channel_added':\n\n                // to do check payload object and insert new channel to store.\n                this.onAddChannel(payload);\n\n                break;\n\n            default:\n\n                break;\n        }\n\n\n    }\n\n    onUpdateUserStatus(userId, isOnline = false){\n\n        const store = this.store;\n\n\n        store.users = store.users.update(userId, (user) => {\n\n\n           if(user){\n               user.online = isOnline;\n\n           }\n\n            return user;\n\n        });\n\n        store.update()\n\n    }\n    onAddMessage(payload, notify = false){\n\n        const store = this.store;\n        const currentUser = store.getCurrentUser();\n        const currentUserId = _.toString(_.get(currentUser, '_id'));\n\n        let user = _.get(payload, 'user');\n\n\n        // add user to cache\n        user = store.addUserToCache(user);\n\n        const messageObject = {\n            _id: payload._id,\n            body: _.get(payload, 'body', ''),\n            userId: _.get(payload, 'userId'),\n            channelId: _.get(payload, 'channelId'),\n            created: _.get(payload, 'created', new Date()),\n            me: currentUserId === _.toString(_.get(payload, 'userId')),\n            user: user,\n\n        };\n\n\n\n        \n\n        store.setMessage(messageObject, notify);\n\n    }\n\n    onAddChannel(payload) {\n\n        const store = this.store;\n\n        const channelId = _.toString(_.get(payload, '_id'));\n        const userId = `${payload.userId}`;\n\n        const users = _.get(payload, 'users', []);\n\n\n        let channel = {\n            _id: channelId,\n            title: _.get(payload, 'title', ''),\n            isNew: false,\n            lastMessage: _.get(payload, 'lastMessage'),\n            members: new OrderedMap(),\n            messages: new OrderedMap(),\n            userId: userId,\n            created: new Date(),\n            \n\n        };\n\n        _.each(users, (user) => {\n\n            // add this user to store.users collection\n\n            const memberId = `${user._id}`;\n\n            this.store.addUserToCache(user);\n\n            channel.members = channel.members.set(memberId, true);\n\n\n        });\n\n\n\n        const channelMessages = store.messages.filter((m) => _.toString(m.channelId)=== channelId);\n\n        channelMessages.forEach((msg) => {\n\n            const msgId = _.toString(_.get(msg, '_id'));\n            channel.messages = channel.messages.set(msgId, true);\n\n        })\n\n\n        store.addChannel(channelId, channel);\n\n    }\n\n    send(msg = {}) {\n\n        const isConnected = this.isConnected;\n\n        if (this.ws && isConnected) {\n\n            const msgString = JSON.stringify(msg);\n\n            this.ws.send(msgString);\n        }\n\n    }\n\n    authentication() {\n        const store = this.store;\n\n        const tokenId = store.getUserTokenId();\n\n        if (tokenId) {\n\n            const message = {\n                action: 'auth',\n                payload: `${tokenId}`\n            }\n\n            this.send(message);\n        }\n\n    }\n\n\n    connect() {\n\n        //console.log(\"Begin connecting to server via websocket.\");\n\n        const ws = new WebSocket(websocketUrl);\n        this.ws = ws;\n\n\n        ws.onopen = () => {\n\n\n            //console.log(\"You are connected\");\n\n            // let tell to the server who are you ?\n\n            this.isConnected = true;\n\n            this.authentication();\n\n\n            ws.onmessage = (event) => {\n\n                this.readMessage(_.get(event, 'data'));\n\n\n                console.log(\"Mesage from the server: \", event.data);\n            }\n\n\n        }\n\n        ws.onclose = () => {\n\n            //console.log(\"You disconnected!!!\");\n            this.isConnected = false;\n            //this.store.update();\n\n        }\n\n        ws.onerror = () => {\n\n            this.isConnected = false;\n            this.store.update();\n        }\n\n\n    }\n}"
  },
  {
    "path": "app/src/registerServiceWorker.js",
    "content": "// In production, we register a service worker to serve assets from local cache.\n\n// This lets the app load faster on subsequent visits in production, and gives\n// it offline capabilities. However, it also means that developers (and users)\n// will only see deployed updates on the \"N+1\" visit to a page, since previously\n// cached resources are updated in the background.\n\n// To learn more about the benefits of this model, read https://goo.gl/KwvDNy.\n// This link also includes instructions on opting out of this behavior.\n\nconst isLocalhost = Boolean(\n  window.location.hostname === 'localhost' ||\n    // [::1] is the IPv6 localhost address.\n    window.location.hostname === '[::1]' ||\n    // 127.0.0.1/8 is considered localhost for IPv4.\n    window.location.hostname.match(\n      /^127(?:\\.(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)){3}$/\n    )\n);\n\nexport default function register() {\n  if (process.env.NODE_ENV === 'production' && 'serviceWorker' in navigator) {\n    // The URL constructor is available in all browsers that support SW.\n    const publicUrl = new URL(process.env.PUBLIC_URL, window.location);\n    if (publicUrl.origin !== window.location.origin) {\n      // Our service worker won't work if PUBLIC_URL is on a different origin\n      // from what our page is served on. This might happen if a CDN is used to\n      // serve assets; see https://github.com/facebookincubator/create-react-app/issues/2374\n      return;\n    }\n\n    window.addEventListener('load', () => {\n      const swUrl = `${process.env.PUBLIC_URL}/service-worker.js`;\n\n      if (isLocalhost) {\n        // This is running on localhost. Lets check if a service worker still exists or not.\n        checkValidServiceWorker(swUrl);\n      } else {\n        // Is not local host. Just register service worker\n        registerValidSW(swUrl);\n      }\n    });\n  }\n}\n\nfunction registerValidSW(swUrl) {\n  navigator.serviceWorker\n    .register(swUrl)\n    .then(registration => {\n      registration.onupdatefound = () => {\n        const installingWorker = registration.installing;\n        installingWorker.onstatechange = () => {\n          if (installingWorker.state === 'installed') {\n            if (navigator.serviceWorker.controller) {\n              // At this point, the old content will have been purged and\n              // the fresh content will have been added to the cache.\n              // It's the perfect time to display a \"New content is\n              // available; please refresh.\" message in your web app.\n              console.log('New content is available; please refresh.');\n            } else {\n              // At this point, everything has been precached.\n              // It's the perfect time to display a\n              // \"Content is cached for offline use.\" message.\n              console.log('Content is cached for offline use.');\n            }\n          }\n        };\n      };\n    })\n    .catch(error => {\n      console.error('Error during service worker registration:', error);\n    });\n}\n\nfunction checkValidServiceWorker(swUrl) {\n  // Check if the service worker can be found. If it can't reload the page.\n  fetch(swUrl)\n    .then(response => {\n      // Ensure service worker exists, and that we really are getting a JS file.\n      if (\n        response.status === 404 ||\n        response.headers.get('content-type').indexOf('javascript') === -1\n      ) {\n        // No service worker found. Probably a different app. Reload the page.\n        navigator.serviceWorker.ready.then(registration => {\n          registration.unregister().then(() => {\n            window.location.reload();\n          });\n        });\n      } else {\n        // Service worker found. Proceed as normal.\n        registerValidSW(swUrl);\n      }\n    })\n    .catch(() => {\n      console.log(\n        'No internet connection found. App is running in offline mode.'\n      );\n    });\n}\n\nexport function unregister() {\n  if ('serviceWorker' in navigator) {\n    navigator.serviceWorker.ready.then(registration => {\n      registration.unregister();\n    });\n  }\n}\n"
  },
  {
    "path": "app/src/service.js",
    "content": "import axios from 'axios'\nimport {apiUrl} from './config'\n\nconst apiURL = apiUrl;\n\nexport default class Service{\n\n\tget(endpoint, options = null){\n\n\t\tconst url = `${apiURL}/${endpoint}`;\n\n\t\treturn axios.get(url, options);\n\t}\n\n\tpost(endpoint = \"\", data = {}, options = {headers: {'Content-Type': 'application/json'}}){\n\n\t\tconst url = `${apiURL}/${endpoint}`;\n\t\t\n\t\treturn axios.post(url, data, options);\n\t}\n\n}"
  },
  {
    "path": "app/src/store.js",
    "content": "import {OrderedMap} from 'immutable'\nimport _ from 'lodash'\nimport Service from './service'\nimport Realtime from './realtime'\n\nexport default class Store {\n    constructor(appComponent) {\n\n        this.app = appComponent;\n        this.service = new Service();\n        this.messages = new OrderedMap();\n        this.channels = new OrderedMap();\n        this.activeChannelId = null;\n\n\n        this.token = this.getTokenFromLocalStore();\n\n        this.user = this.getUserFromLocalStorage();\n        this.users = new OrderedMap();\n\n        this.search = {\n            users: new OrderedMap(),\n        }\n\n\n        this.realtime = new Realtime(this);\n\n        this.fetchUserChannels();\n\n\n    }\n\n    isConnected(){\n\n        return this.realtime.isConnected;\n    }\n    fetchUserChannels(){\n\n        const userToken = this.getUserTokenId();\n\n        if(userToken){\n\n\n            const options = {\n                headers: {\n                    authorization: userToken,\n                }\n            }\n\n            this.service.get(`api/me/channels`, options).then((response) => {\n\n                const channels = response.data;\n\n                _.each(channels, (c) => {\n\n                    this.realtime.onAddChannel(c);\n                });\n\n\n                const firstChannelId = _.get(channels, '[0]._id', null);\n\n                this.fetchChannelMessages(firstChannelId);\n\n\n            }).catch((err) => {\n\n                console.log(\"An error fetching user channels\", err);\n            })\n        }\n    }\n\n    addUserToCache(user) {\n\n        user.avatar = this.loadUserAvatar(user);\n        const id = _.toString(user._id);\n        this.users = this.users.set(id, user);\n\n\n        return user;\n\n\n    }\n\n    getUserTokenId() {\n        return _.get(this.token, '_id', null);\n    }\n\n    loadUserAvatar(user) {\n\n        return `https://api.adorable.io/avatars/100/${user._id}.png`\n    }\n\n    startSearchUsers(q = \"\") {\n\n        // query to backend servr and get list of users.\n        const data = {search: q};\n\n        this.search.users = this.search.users.clear();\n\n        this.service.post('api/users/search', data).then((response) => {\n\n            // list of users matched.\n            const users = _.get(response, 'data', []);\n\n            _.each(users, (user) => {\n\n                // cache to this.users\n                // add user to this.search.users \n\n                user.avatar = this.loadUserAvatar(user);\n                const userId = `${user._id}`;\n\n                this.users = this.users.set(userId, user);\n                this.search.users = this.search.users.set(userId, user);\n\n\n            });\n\n\n            // update component\n            this.update();\n\n\n        }).catch((err) => {\n\n\n            //console.log(\"searching errror\", err);\n        })\n\n    }\n\n    setUserToken(accessToken) {\n\n        if (!accessToken) {\n\n            this.localStorage.removeItem('token');\n            this.token = null;\n\n            return;\n        }\n\n        this.token = accessToken;\n        localStorage.setItem('token', JSON.stringify(accessToken));\n\n    }\n\n    getTokenFromLocalStore() {\n\n\n        if (this.token) {\n            return this.token;\n        }\n\n        let token = null;\n\n        const data = localStorage.getItem('token');\n        if (data) {\n\n            try {\n\n                token = JSON.parse(data);\n            }\n            catch (err) {\n\n                console.log(err);\n            }\n        }\n\n        return token;\n    }\n\n    getUserFromLocalStorage() {\n\n        let user = null;\n        const data = localStorage.getItem('me');\n        try {\n\n            user = JSON.parse(data);\n        }\n        catch (err) {\n\n            console.log(err);\n        }\n\n\n        if (user) {\n\n            // try to connect to backend server and verify this user is exist.\n            const token = this.getTokenFromLocalStore();\n            const tokenId = _.get(token, '_id');\n\n            const options = {\n                headers: {\n                    authorization: tokenId,\n                }\n            }\n            this.service.get('api/users/me', options).then((response) => {\n\n                // this mean user is logged with this token id.\n\n                const accessToken = response.data;\n                const user = _.get(accessToken, 'user');\n\n                this.setCurrentUser(user);\n                this.setUserToken(accessToken);\n\n            }).catch(err => {\n\n                this.signOut();\n\n            });\n\n        }\n        return user;\n    }\n\n    setCurrentUser(user) {\n\n\n        // set temporary user avatar image url\n        user.avatar = this.loadUserAvatar(user);\n        this.user = user;\n\n\n        if (user) {\n            localStorage.setItem('me', JSON.stringify(user));\n\n            // save this user to our users collections in local \n            const userId = `${user._id}`;\n            this.users = this.users.set(userId, user);\n        }\n\n        this.update();\n\n    }\n\n    clearCacheData(){\n\n        this.channels = this.channels.clear();\n        this.messages = this.messages.clear();\n        this.users = this.users.clear();\n    }\n    signOut() {\n\n        const userId = _.toString(_.get(this.user, '_id', null));\n        const tokenId = _.get(this.token, '_id', null); //this.token._id;\n        // request to backend and loggout this user\n\n        const options = {\n            headers: {\n                authorization: tokenId,\n            }\n        };\n\n        this.service.get('api/me/logout', options);\n\n        this.user = null;\n        localStorage.removeItem('me');\n        localStorage.removeItem('token');\n\n        this.clearCacheData();\n\n        if (userId) {\n            this.users = this.users.remove(userId);\n        }\n\n        this.update();\n    }\n\n    register(user){\n\n        return new Promise((resolve, reject) => {\n\n            this.service.post('api/users', user).then((response) => {\n\n                console.log(\"use created\", response.data);\n\n                return resolve(response.data);\n            }).catch(err => {\n\n                return reject(\"An error create your account\");\n            })\n\n\n        });\n    }\n    login(email = null, password = null) {\n\n        const userEmail = _.toLower(email);\n\n\n        const user = {\n            email: userEmail,\n            password: password,\n        }\n        //console.log(\"Ttrying to login with user info\", user);\n\n\n        return new Promise((resolve, reject) => {\n\n\n            // we call to backend service and login with user data\n\n            this.service.post('api/users/login', user).then((response) => {\n\n                // that mean successful user logged in\n\n                const accessToken = _.get(response, 'data');\n                const user = _.get(accessToken, 'user');\n\n                this.setCurrentUser(user);\n                this.setUserToken(accessToken);\n\n                // call to realtime and connect again to socket server with this user\n\n                this.realtime.connect();\n\n                // begin fetching user's channels\n\n                this.fetchUserChannels();\n\n                //console.log(\"Got user login callback from the server: \", accessToken);\n\n\n            }).catch((err) => {\n\n                console.log(\"Got an error login from server\", err);\n                // login error\n\n                const message = _.get(err, 'response.data.error.message', \"Login Error!\");\n\n                return reject(message);\n            })\n\n        });\n\n\n    }\n\n    removeMemberFromChannel(channel = null, user = null) {\n\n        if (!channel || !user) {\n            return;\n        }\n\n        const userId = _.get(user, '_id');\n        const channelId = _.get(channel, '_id');\n\n        channel.members = channel.members.remove(userId);\n\n        this.channels = this.channels.set(channelId, channel);\n\n        this.update();\n\n    }\n\n    addUserToChannel(channelId, userId) {\n\n\n        const channel = this.channels.get(channelId);\n\n        if (channel) {\n\n            // now add this member id to channels members.\n            channel.members = channel.members.set(userId, true);\n            this.channels = this.channels.set(channelId, channel);\n            this.update();\n        }\n\n    }\n\n    getSearchUsers() {\n\n        return this.search.users.valueSeq();\n    }\n\n    onCreateNewChannel(channel = {}) {\n\n        const channelId = _.get(channel, '_id');\n        this.addChannel(channelId, channel);\n        this.setActiveChannelId(channelId);\n\n        //console.log(JSON.stringify(this.channels.toJS()));\n\n    }\n\n    getCurrentUser() {\n\n        return this.user;\n    }\n\n\n    fetchChannelMessages(channelId){\n\n\n        let channel = this.channels.get(channelId);\n\n        if (channel && !_.get(channel, 'isFetchedMessages')){\n\n            const token = _.get(this.token, '_id');//this.token._id;\n            const options = {\n                headers: {\n                    authorization: token,\n                }\n            }\n\n             this.service.get(`api/channels/${channelId}/messages`, options).then((response) => {\n\n\n\n                    channel.isFetchedMessages = true;\n\n                    const messages = response.data;\n\n                    _.each(messages, (message) => {\n\n                            this.realtime.onAddMessage(message);\n\n                    });\n\n\n                    this.channels = this.channels.set(channelId, channel);\n\n\n\n\n            }).catch((err) => {\n\n                console.log(\"An error fetching channel 's messages\", err);\n            })\n\n\n        }\n        \n    }\n    setActiveChannelId(id) {\n\n        this.activeChannelId = id;\n\n        this.fetchChannelMessages(id);\n\n        this.update();\n\n    }\n\n\n    getActiveChannel() {\n\n        const channel = this.activeChannelId ? this.channels.get(this.activeChannelId) : this.channels.first();\n        return channel;\n\n    }\n\n    setMessage(message, notify = false) {\n\n        const id = _.toString(_.get(message, '_id'));\n        this.messages = this.messages.set(id, message);\n        const channelId = _.toString(message.channelId);\n        const channel = this.channels.get(channelId);\n\n        if (channel) {\n            channel.messages = channel.messages.set(id, true);\n            channel.lastMessage = _.get(message, 'body', '');\n            channel.notify = notify;\n\n            this.channels = this.channels.set(channelId, channel);\n        } else {\n\n            // fetch to the server with channel info\n            this.service.get(`api/channels/${channelId}`).then((response) => {\n\n\n                const channel = _.get(response, 'data');\n\n                /*const users = _.get(channel, 'users');\n                _.each(users, (user) => {\n\n                    this.addUserToCache(user);\n                });*/\n\n                this.realtime.onAddChannel(channel);\n\n\n            })\n        }\n        this.update();\n    }\n\n    addMessage(id, message = {}) {\n\n        // we need add user object who is author of this message\n\n\n        const user = this.getCurrentUser();\n        message.user = user;\n\n        this.messages = this.messages.set(id, message);\n\n        // let's add new message id to current channel->messages.\n\n        const channelId = _.get(message, 'channelId');\n        if (channelId) {\n\n            let channel = this.channels.get(channelId);\n\n\n            channel.lastMessage = _.get(message, 'body', '');\n\n            // now send this channel info to the server\n            const obj = {\n\n                action: 'create_channel',\n                payload: channel,\n            };\n            this.realtime.send(obj);\n\n\n            //console.log(\"channel:\", channel);\n\n            // send to the server via websocket to creawte new message and notify to other members.\n\n            this.realtime.send(\n                {\n                    action: 'create_message',\n                    payload: message,\n                }\n            );\n\n            channel.messages = channel.messages.set(id, true);\n\n\n            channel.isNew = false;\n            this.channels = this.channels.set(channelId, channel);\n\n\n        }\n        this.update();\n\n        // console.log(JSON.stringify(this.messages.toJS()));\n\n    }\n\n    getMessages() {\n\n        return this.messages.valueSeq();\n    }\n\n    getMessagesFromChannel(channel) {\n\n        let messages = new OrderedMap();\n\n\n        if (channel) {\n\n\n            channel.messages.forEach((value, key) => {\n\n\n                const message = this.messages.get(key);\n\n                messages = messages.set(key, message);\n\n            });\n\n        }\n\n        return messages.valueSeq();\n    }\n\n    getMembersFromChannel(channel) {\n\n        let members = new OrderedMap();\n\n        if (channel) {\n\n\n            channel.members.forEach((value, key) => {\n\n\n                const userId = `${key}`;\n                const user = this.users.get(userId);\n\n                const loggedUser = this.getCurrentUser();\n\n                if (_.get(loggedUser, '_id') !== _.get(user, '_id')) {\n                    members = members.set(key, user);\n                }\n\n\n            });\n        }\n\n        return members.valueSeq();\n    }\n\n    addChannel(index, channel = {}) {\n        this.channels = this.channels.set(`${index}`, channel);\n\n        this.update();\n    }\n\n    getChannels() {\n\n        //return this.channels.valueSeq();\n\n        // we need to sort channel by date , the last one will list on top.\n\n\n        this.channels = this.channels.sort((a, b) => a.updated < b.updated);\n\n        return this.channels.valueSeq();\n    }\n\n    update() {\n\n        this.app.forceUpdate()\n    }\n}"
  },
  {
    "path": "deployment-to-digitalocean-hosting.md",
    "content": "# Deploy Reactjs, Nodejs Chat app to DigitalOcean hosting (Ubuntu VPS)\n\n## Get DigitalOcean account\n\nI 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.\nBesides, their support is very fast and they have good documentation, and a friendly UI for end-users. \nSo lets get started by registering an account and deploy your app at <a href=\"https://m.do.co/c/bb792e37b9dd\">Digitalocean.com</a>\n\n## Setup Ubuntu on DigitalOcean Cloud VPS.\n\n* 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.\n* 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.\n* Select additional options if you want to have additional backup service, or private network. \n* 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>\n* By default, you create one droplet at a time. If you want to, you can set up multiple droplets at once.\n* 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.\n* Check your email that you registered with on DigitalOcean. You should receive an email notifying you about your VPS IP, root username and password.\nThis is the format of the email.\nDroplet Name: [Name of your Droplet]\nIP Address: [your-VPS-IP]\nUsername: root\nPassword: [your-root-password-generated-by-robot]\n* Login to your Cloud via terminal by writing \n  ```\n  ssh root@YOUR-IP-ADDREESS \n  ```\n  Now, enter the root password given in the email. You will be asked for a new password the first time logging in.\n  + The server will ask you for your password once more (the password given in the email).\n  + Enter a new password\n  + Confirm the password, and remember it for later\n  + The setup is complete.\n\n## Configuring the Firewall on your Cloud\n\nThis is a very important step we need to do. We need to reconfigure our firewall software to allow access to the service\n* 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.\n* By default Firewall is inactive, which you can check by running \n``` sudo ufw status ```\n\n  ```\n  sudo ufw app list\n  ```\n* So let us config the Firewall and allow those ports by\n  ```\n  sudo ufw allow 'Nginx Full'\n  ```\n  \n  ```\n  sudo ufw allow 'OpenSSH'\n  ```\n  \n  ```\n  sudo ufw enable\n  ```\n\n## Setup Nodejs on DigitalOcean Ubuntu 16.04 \nWe are using Nodejs for backend and we will serve the static files of the react application build. So Nodejs is required\n* Visit https://nodejs.org/en/download/package-manager/ to see the documentation\n* We use package management to install, here is command to install Node.js v9\n\n  ```\n  curl -sL https://deb.nodesource.com/setup_9.x | sudo -E bash -\n  ```\n  ```\n  sudo apt-get install -y nodejs\n  ```\n* 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).\n\n## Setup MongoDB v3.6 on DigitalOcean Ubuntu 16.04 Cloud VPS\n\nWe 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/\n\n* Import the public key used by the package management system\n  ```\n  sudo apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 2930ADAE8CAF5059EE73BB4B58712A2291FA4AD5\n  ```\n* Create a list file for MongoDB (Ubuntu 16.04)\n  ```\n  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\n  ```\n* Reload local package database\n  ```\n  sudo apt-get update\n  ```\n* Install the latest stable version of MongoDB \n  ```\n  sudo apt-get install -y mongodb-org\n  ```\n* Start MongoDB by running (default port: 27017)\n  ```\n  sudo service mongod start\n  ```\n* Stop MongoDB by running\n  ```\n  sudo service mongod stop\n  ```\n* Restart MongoDB by running\n  ```\n  sudo service mongod restart\n  ```\n \n## Install Nginx - Http Proxy Server\nLet me explain simply why we use Nginx for this Nodejs web application.\nWhen 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.\nIn 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. \n\n* To install Nginx, visit the official documentation at http://nginx.org/en/linux_packages.html \n* So we will run following command on Ubuntu cloud VPS 16.04\n  ```\n  apt-get update\n  ```\n  \n  ```\n  sudo apt-get install nginx\n  ```\n* 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\n  ```\n  nginx\n  ```\n* Stop Nginx \n   ```\n   nginx -s stop\n   ```\n* Reload Nginx\n  ```\n  nginx -s reload\n  ```\n* Close your Cloud command line by ``` exit ``` or cloud command line tab in terminal\n\n## Time to Deployment \n\n* Download the chat app project at https://github.com/tabvn/nodejs-reactjs-chatapp.\n  ```\n  git clone https://github.com/tabvn/nodejs-reactjs-chatapp.git chatApp\n  ```\n  ```\n  cd chatApp\n  ```\n  ```\n  cd server\n  ```\n  ```\n  npm install\n  ```\n  ```\n  cd ../app\n  ```\n  ```\n  npm install\n  ```\n\n* Fixed issue of bcrypt on Ubuntu 16.04\n\n```\nsudo apt-get install build-essential\n\n```\n\n\n## Nginx config sample:\nNginx Websocket document: http://nginx.org/en/docs/http/websocket.html\n\n```\nserver {\n        listen 80;\n        root /var/www/html;\n        location / {\n\n\n                proxy_pass http://127.0.0.1:3001;\n                proxy_http_version 1.1;\n                proxy_set_header Upgrade $http_upgrade;\n                proxy_set_header Connection \"upgrade\";\n        }\n\n}\n```\nSee Video: https://www.youtube.com/watch?v=wJsH45eWNBo\n\n"
  },
  {
    "path": "server/Dockerfile",
    "content": "FROM ubuntu:18.04\n#RUN apk add --update \\ libc6-compat\n# Install a bunch of node modules that are commonly used.\n#ADD package.json /usr/app/\nRUN apt-get update && apt-get -qq -y install curl\n\n\nENV NODE_VERSION=9.9.0\nRUN apt-get install -y curl\nRUN curl -o- https://raw.githubusercontent.com/creationix/nvm/v0.34.0/install.sh | bash\nENV NVM_DIR=/root/.nvm\nRUN . \"$NVM_DIR/nvm.sh\" && nvm install ${NODE_VERSION}\nRUN . \"$NVM_DIR/nvm.sh\" && nvm use v${NODE_VERSION}\nRUN . \"$NVM_DIR/nvm.sh\" && nvm alias default v${NODE_VERSION}\nENV PATH=\"/root/.nvm/versions/node/v${NODE_VERSION}/bin/:${PATH}\"\nRUN node --version\nRUN npm --version\nADD . /usr/app/\n\nWORKDIR /usr/app\n#RUN npm install -g n\n#RUN n 8.4.0\n\n\n#RUN npm install uNetworking/uWebSockets.js#v15.11.0\nRUN npm install\n#RUN npm rebuild uws \n"
  },
  {
    "path": "server/docker-compose.yml",
    "content": "version: '3'\nservices:\n  server:\n    build: .\n    ports:\n      - \"3001:3001\"\n    depends_on:\n      - mongo\n    command: npm run dev\n  mongo:\n    image: mongo\n    ports:\n    - \"27017:27017\"    \n\n  "
  },
  {
    "path": "server/package.json",
    "content": "{\n  \"name\": \"chatapp\",\n  \"version\": \"1.0.0\",\n  \"description\": \"Use websocket in application.\",\n  \"main\": \"index.js\",\n  \"scripts\": {\n    \"dev\": \"nodemon -w src --exec \\\"babel-node src --presets env,stage-0\\\"\",\n    \"build\": \"babel src -s -D -d dist --presets env,stage-0\",\n    \"start\": \"node dist\",\n    \"prestart\": \"npm run -s build\",\n    \"test\": \"eslint src\"\n  },\n  \"eslintConfig\": {\n    \"extends\": \"eslint:recommended\",\n    \"parserOptions\": {\n      \"ecmaVersion\": 7,\n      \"sourceType\": \"module\"\n    },\n    \"env\": {\n      \"node\": true\n    },\n    \"rules\": {\n      \"no-console\": 0,\n      \"no-unused-vars\": 1\n    }\n  },\n  \"dependencies\": {\n    \"bcrypt\": \"^2.0.0\",\n    \"body-parser\": \"^1.18.2\",\n    \"cors\": \"^2.8.4\",\n    \"express\": \"^4.16.2\",\n    \"immutable\": \"^3.8.2\",\n    \"lodash\": \"^4.17.4\",\n    \"moment\": \"^2.19.3\",\n    \"mongodb\": \"^2.2.33\",\n    \"morgan\": \"^1.9.0\",\n    \"uws\": \"^9.14.0\"\n  },\n  \"devDependencies\": {\n    \"babel-cli\": \"^6.26.0\",\n    \"babel-core\": \"^6.26.0\",\n    \"babel-preset-env\": \"^1.6.1\",\n    \"babel-preset-stage-0\": \"^6.24.1\",\n    \"eslint\": \"^4.9.0\",\n    \"nodemon\": \"^1.12.1\"\n  },\n  \"author\": \"toan@tabvn.com\",\n  \"license\": \"ISC\"\n}\n"
  },
  {
    "path": "server/src/app-router.js",
    "content": "import moment from 'moment';\nimport _ from 'lodash'\n\n\nexport const START_TIME = new Date();\n\nexport default class AppRouter {\n\n\n    constructor(app) {\n\n        this.app = app;\n\n\n        this.setupRouter = this.setupRouter.bind(this);\n\n\n        this.setupRouter();\n    }\n\n    setupRouter() {\n\n        const app = this.app;\n\n        console.log(\"APp ROuter works!\");\n\n\n\n        /**\n         * @endpoint: /api/users\n         * @method: POST\n         **/\n        app.post('/api/users', (req, res, next) => {\n\n            const body = req.body;\n\n            app.models.user.create(body).then((user) => {\n\n                _.unset(user, 'password');\n\n                return res.status(200).json(user);\n\n            }).catch(err => {\n\n\n                return res.status(503).json({error: err});\n            })\n\n\n        });\n\n\n        /**\n         * @endpoint: /api/users/me\n         * @method: GET\n         **/\n\n        app.get('/api/users/me', (req, res, next) => {\n\n            let tokenId = req.get('authorization');\n\n            if (!tokenId) {\n                // get token from query\n\n                tokenId = _.get(req, 'query.auth');\n            }\n\n\n            app.models.token.loadTokenAndUser(tokenId).then((token) => {\n                _.unset(token, 'user.password');\n\n                return res.json(token);\n\n            }).catch(err => {\n\n                return res.status(401).json({\n                    error: err\n                })\n            });\n\n\n        });\n\n\n        /**\n         * @endpoint: /api/users/search\n         * @method: POST\n         **/\n\n        app.post('/api/users/search', (req, res, next) => {\n\n\n            const keyword = _.get(req, 'body.search', '');\n\n            app.models.user.search(keyword).then((results) => {\n\n\n                return res.status(200).json(results);\n            }).catch((err) => {\n\n                return res.status(404).json({\n                    error: 'Not found.'\n                })\n            })\n\n        });\n\n\n        /**\n         * @endpoint: /api/users/:id\n         * @method: GET\n         **/\n\n        app.get('/api/users/:id', (req, res, next) => {\n\n            const userId = _.get(req, 'params.id');\n\n\n            app.models.user.load(userId).then((user) => {\n\n                _.unset(user, 'password');\n\n                return res.status(200).json(user);\n            }).catch(err => {\n\n                return res.status(404).json({\n                    error: err,\n                })\n            })\n\n\n        });\n\n\n        /**\n         * @endpoint: /api/users/login\n         * @method: POST\n         **/\n\n        app.post('/api/users/login', (req, res, next) => {\n\n            const body = _.get(req, 'body');\n\n\n            app.models.user.login(body).then((token) => {\n\n\n                _.unset(token, 'user.password');\n\n                return res.status(200).json(token);\n\n\n            }).catch(err => {\n\n                return res.status(401).json({\n                    error: err\n                })\n            })\n\n        })\n\n\n        /**\n         * @endpoint: /api/channels/:id\n         * @method: GET\n         **/\n\n\n        app.get('/api/channels/:id', (req, res, next) => {\n\n            const channelId = _.get(req, 'params.id');\n\n            console.log(channelId);\n\n            if (!channelId) {\n\n                return res.status(404).json({error: {message: \"Not found.\"}});\n            }\n\n\n            app.models.channel.load(channelId).then((channel) => {\n\n                // fetch all uses belong to memberId\n\n                const members = channel.members;\n                const query = {\n                    _id: {$in: members}\n                };\n                const options = {_id: 1, name: 1, created: 1};\n\n                app.models.user.find(query, options).then((users) => {\n                    channel.users = users;\n\n                    return res.status(200).json(channel);\n                }).catch(err => {\n\n                    return res.status(404).json({error: {message: \"Not found.\"}});\n\n                });\n\n\n            }).catch((err) => {\n\n                return res.status(404).json({error: {message: \"Not found.\"}});\n            })\n\n\n        });\n\n\n        /**\n         * @endpoint: /api/channels/:id/messages\n         * @method: GET\n         **/\n\n        app.get('/api/channels/:id/messages', (req, res, next) => {\n\n\n            let tokenId = req.get('authorization');\n\n            if (!tokenId) {\n                // get token from query\n\n                tokenId = _.get(req, 'query.auth');\n            }\n\n\n            app.models.token.loadTokenAndUser(tokenId).then((token) => {\n\n\n                const userId = token.userId;\n\n\n                // make sure user are logged in\n                // check if this user is inside of channel members. other retun 401.\n\n                let filter = _.get(req, 'query.filter', null);\n                if (filter) {\n\n                    filter = JSON.parse(filter);\n                    console.log(filter);\n                }\n\n                const channelId = _.toString(_.get(req, 'params.id'));\n                const limit = _.get(filter, 'limit', 50);\n                const offset = _.get(filter, 'offset', 0);\n\n\n                // load channel\n\n                this.app.models.channel.load(channelId).then((c) => {\n\n\n                    const memberIds = _.get(c, 'members');\n\n                    const members = [];\n\n                    _.each(memberIds, (id) => {\n                        members.push(_.toString(id));\n                    })\n\n\n                    if (!_.includes(members, _.toString(userId))) {\n\n                        return res.status(401).json({error: {message: \"Access denied\"}});\n                    }\n\n                    this.app.models.message.getChannelMessages(channelId, limit, offset).then((messages) => {\n\n\n                        return res.status(200).json(messages);\n\n                    }).catch((err) => {\n\n                        return res.status(404).json({error: {message: \"Not found.\"}});\n                    })\n\n\n                }).catch((err) => {\n\n                    return res.status(404).json({error: {message: \"Not found.\"}});\n\n                })\n\n\n            }).catch((err) => {\n\n\n                return res.status(401).json({error: {message: \"Access denied\"}});\n\n\n            });\n\n\n        });\n\n\n        /**\n         * @endpoint: /api/me/channels\n         * @method: GET\n         **/\n\n        app.get('/api/me/channels', (req, res, next) => {\n\n\n            let tokenId = req.get('authorization');\n\n            if (!tokenId) {\n                // get token from query\n\n                tokenId = _.get(req, 'query.auth');\n            }\n\n\n            app.models.token.loadTokenAndUser(tokenId).then((token) => {\n\n\n                const userId = token.userId;\n\n\n                const query = [\n\n                    {\n                        $lookup: {\n                            from: 'users',\n                            localField: 'members',\n                            foreignField: '_id',\n                            as: 'users',\n                        }\n                    },\n                    {\n                        $match: {\n                            members: {$all: [userId]}\n                        }\n                    },\n                    {\n                        $project: {\n                            _id: true,\n                            title: true,\n                            lastMessage: true,\n                            created: true,\n                            updated: true,\n                            userId: true,\n                            users: {\n                                _id: true,\n                                name: true,\n                                created: true,\n                                online: true\n                            },\n                            members: true,\n                        }\n                    },\n                    {\n                        $sort: {updated: -1, created: -1}\n                    },\n                    {\n                        $limit: 50,\n                    }\n                ];\n\n                app.models.channel.aggregate(query).then((channels) => {\n\n\n                    return res.status(200).json(channels);\n\n\n                }).catch((err) => {\n\n                    return res.status(404).json({error: {message: \"Not found.\"}});\n                })\n\n\n            }).catch(err => {\n\n                return res.status(401).json({\n                    error: \"Access denied.\"\n                })\n            });\n\n\n        });\n\n\n\n\n        /**\n         * @endpoint: /api/me/logout\n         * @method: GET\n         **/\n\n        app.get('/api/me/logout', (req, res, next) => {\n\n            let tokenId = req.get('authorization');\n\n            if (!tokenId) {\n                // get token from query\n\n                tokenId = _.get(req, 'query.auth');\n            }\n\n\n            app.models.token.loadTokenAndUser(tokenId).then((token) => {\n\n\n                app.models.token.logout(token);\n\n                return res.status(200).json({\n                    message: 'Successful.'\n                });\n\n            }).catch(err => {\n\n\n                return res.status(401).json({error: {message: 'Access denied'}});\n            })\n\n\n\n        })\n\n\n    }\n}\n\n\n\n\n\n\n\n\n\n\n\n\n\n"
  },
  {
    "path": "server/src/database.js",
    "content": "import {MongoClient} from 'mongodb'\n\nconst URL = 'mongodb://mongo:27017';\n\n\nexport default class Database{\n\n\tconnect(){\n\n\n\t\treturn new Promise((resolve, reject) => {\n\n\t\t\tMongoClient.connect(URL, (err, db) => {\n\t\t\t\t\n\t\t\t\treturn err ? reject(err) : resolve(db);\n\n\t\t\t});\n\n\n\t\t});\n\n\n\n\t}\n}"
  },
  {
    "path": "server/src/helper.js",
    "content": "export const isEmail = (emaill) => {\n\n\n\tconst 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,}))$/\n\n\treturn regex.test(emaill);\n}\n\nexport const toString = (id = \"\") => {\n\n\n\treturn `${id}`;\n\t\t\n\n}"
  },
  {
    "path": "server/src/index.js",
    "content": "import http from 'http';\nimport express from 'express';\nimport cors from 'cors';\nimport bodyParser from 'body-parser';\nimport {version} from '../package.json'\nimport WebSocketServer, {Server} from 'uws';\nimport AppRouter from './app-router'\nimport Model from './models'\nimport Database from './database'\nimport path from 'path'\n\nconst PORT = 3001;\nconst app = express();\napp.server = http.createServer(app);\n\n\n//app.use(morgan('dev'));\n\n\napp.use(cors({\n    exposedHeaders: \"*\"\n}));\n\napp.use(bodyParser.json({\n    limit: '50mb'\n}));\n\n\n\napp.wss = new Server({\n\tserver: app.server\n});\n\n\n// static www files use express\nconst wwwPath = path.join(__dirname, 'www');\n\napp.use('/', express.static(wwwPath));\n\n// Connect to Mongo Database\n\nnew Database().connect().then((db) => {\n\n\tconsole.log(\"Successful connected to database.\")\n\n\tapp.db = db;\n\t\n}).catch((err) => {\n\n\n\tthrow(err);\n});\n\n\n// End connect to Mongodb Database\n\napp.models = new Model(app);\napp.routers = new AppRouter(app);\n\n\n\n\n\napp.server.listen(process.env.PORT || PORT, () => {\n        console.log(`App is running on port ${app.server.address().port}`);\n});\n\nexport default app;"
  },
  {
    "path": "server/src/models/channel.js",
    "content": "import _ from 'lodash'\nimport {toString} from '../helper'\nimport {ObjectID} from 'mongodb'\nimport {OrderedMap} from 'immutable'\n\nexport default class Channel {\n\n    constructor(app) {\n\n        this.app = app;\n\n        this.channels = new OrderedMap();\n    }\n\n\n    aggregate(q){\n\n        return new Promise((resolve, reject) => {\n\n\n            this.app.db.collection('channels').aggregate(q, (err, results) => {\n\n\n                    return err ? reject(err) : resolve(results);\n\n            });\n\n\n        })\n\n    }\n    find(q, options = {}){\n\n\n\n        return new Promise((resolve, reject) => {\n\n\n            this.app.db.collection('channels').find(q, options).toArray((err, results) => {\n\n\n                return err ? reject(err) : resolve(results);\n            });\n\n\n        });\n    }\n    load(id) {\n\n        return new Promise((resolve, reject) => {\n\n\n            id = _.toString(id);\n\n\n            // first find in cache\n            const channelFromCache = this.channels.get(id);\n\n            if (channelFromCache) {\n\n\t\t\t\treturn resolve(channelFromCache);\n            }\n\n\n            // let find in db\n\n            this.findById(id).then((c) => {\n\n                this.channels = this.channels.set(id, c);\n                \n                return resolve(c);\n\n            }).catch((err) => {\n\n\n                return reject(err);\n            })\n\n\n\n        })\n\n    }\n\n    findById(id){\n\n        return new Promise((resolve, reject) => {\n\n\n            this.app.db.collection('channels').findOne({_id: new ObjectID(id)}, (err, result) => {\n\n                    if(err || !result){\n\n                        return reject(err ? err : \"Not found\");\n                    }\n\n                    return resolve(result);\n\n            });\n\n\n        })\n    }\n    create(obj) {\n\n\n        return new Promise((resolve, reject) => {\n\n            let id = toString(_.get(obj, '_id'));\n\n\n            let idObject = id ? new ObjectID(id) : new ObjectID();\n\n\n            let members = [];\n\n            _.each(_.get(obj, 'members', []), (value, key) => {\n\n\n                const memberObjectId = new ObjectID(key);\n                members.push(memberObjectId);\n            });\n\n\n            let userIdObject = null;\n\n            let userId = _.get(obj, 'userId', null);\n            if (userId) {\n                userIdObject = new ObjectID(userId);\n            }\n\n\n            const channel = {\n\n                _id: idObject,\n                title: _.get(obj, 'title', ''),\n                lastMessage: _.get(obj, 'lastMessage', ''),\n                created: new Date(),\n                userId: userIdObject,\n                members: members,\n            }\n\n\n            this.app.db.collection('channels').insertOne(channel, (err, info) => {\n\n                if (!err) {\n\n                    const channelId = channel._id.toString();\n\n                    this.channels = this.channels.set(channelId, channel);\n                }\n                return err ? reject(err) : resolve(channel);\n            });\n\n\n        });\n\n\n    }\n}"
  },
  {
    "path": "server/src/models/connection.js",
    "content": "import {OrderedMap} from 'immutable'\nimport {ObjectID} from 'mongodb'\nimport _ from 'lodash'\n\nexport default class Connection {\n\n    constructor(app) {\n\n        this.app = app;\n\n        this.connections = OrderedMap();\n\n        this.modelDidLoad();\n    }\n\n\n    decodeMesasge(msg) {\n\n\n        let messageObject = null;\n\n\n        try {\n\n            messageObject = JSON.parse(msg);\n        }\n        catch (err) {\n\n            console.log(\"An error decode the socket mesage\", msg);\n        }\n\n\n        return messageObject;\n\n    }\n\n    sendToMembers(userId, obj) {\n\n        const query = [\n            {\n                $match: {\n\n                    members: {$all: [new ObjectID(userId)]}\n                }\n            },\n            {\n\n                $lookup: {\n\n                    from: 'users',\n                    localField: 'members',\n                    foreignField: '_id',\n                    as: 'users'\n                }\n            },\n            {\n                $unwind: {\n\n                    path: '$users'\n                }\n            },\n            {\n                $match: {'users.online': {$eq: true}}\n            },\n            {\n                $group: {\n\n                    _id: \"$users._id\"\n                }\n            }\n\n\n        ];\n\n\n        const users = [];\n\n\n        this.app.db.collection('channels').aggregate(query, (err, results) => {\n\n\n           // console.log(\"found members array who is chattting with current user\", results);\n            if (err === null && results) {\n\n                _.each(results, (result) => {\n\n\n                    const uid = _.toString(_.get(result, '_id'));\n                    if (uid) {\n                        users.push(uid);\n                    }\n                });\n\n\n                // this is list of all connections is chatting with current user\n                const memberConnections = this.connections.filter((con) => _.includes(users, _.toString(_.get(con, 'userId'))));\n                if (memberConnections.size) {\n\n                    memberConnections.forEach((connection, key) => {\n\n                        const ws = connection.ws;\n                        this.send(ws, obj);\n                    });\n                }\n\n\n            }\n        })\n    }\n\n    sendAll(obj) {\n\n\n        // send socket messages to all clients.\n\n        this.connections.forEach((con, key) => {\n            const ws = con.ws;\n\n            this.send(ws, obj);\n        });\n    }\n\n    send(ws, obj) {\n\n        const message = JSON.stringify(obj);\n\n        ws.send(message);\n    }\n\n    doTheJob(socketId, msg) {\n\n\n        const action = _.get(msg, 'action');\n        const payload = _.get(msg, 'payload');\n        const userConnection = this.connections.get(socketId);\n\n        switch (action) {\n\n\n            case 'create_message':\n\n                if (userConnection.isAuthenticated) {\n                    let messageObject = payload;\n\n                    messageObject.userId = _.get(userConnection, 'userId');\n                    //console.log(\"Got message from client about creating new message\", payload);\n\n                    this.app.models.message.create(messageObject).then((message) => {\n\n\n                        // console.log(\"Mesage crewated\", message);\n\n                        const channelId = _.toString(_.get(message, 'channelId'));\n                        this.app.models.channel.load(channelId).then((channel) => {\n\n                            // console.log(\"got channel of the message created\", channel);\n\n                            const memberIds = _.get(channel, 'members', []);\n\n                            _.each(memberIds, (memberId) => {\n\n                                memberId = _.toString(memberId);\n\n                                const memberConnections = this.connections.filter((c) => _.toString(c.userId) === memberId);\n\n\n                                memberConnections.forEach((connection) => {\n\n\n                                    const ws = connection.ws;\n\n                                    this.send(ws, {\n\n                                        action: 'message_added',\n                                        payload: message,\n                                    })\n\n\n                                })\n\n\n                            });\n                        })\n\n                        // message created successful.\n\n\n                    }).catch(err => {\n\n\n                        // send back to the socket client who sent this messagse with error\n                        const ws = userConnection.ws;\n                        this.send(ws, {\n                            action: 'create_message_error',\n                            payload: payload,\n                        })\n                    })\n                }\n\n\n                break;\n            case 'create_channel':\n\n                let channel = payload;\n\n\n                const userId = userConnection.userId;\n                channel.userId = userId;\n\n                this.app.models.channel.create(channel).then((chanelObject) => {\n\n                    // successful created channel ,\n\n                    //console.log(\"Succesful created new channel\", typeof userId, chanelObject);\n\n                    // let send back to all members in this channel  with new channel  created\n                    let memberConnections = [];\n\n                    const memberIds = _.get(chanelObject, 'members', []);\n\n                    // fetch all users has memberId\n\n                    const query = {\n                        _id: {$in: memberIds}\n                    };\n\n                    const queryOptions = {\n                        _id: 1,\n                        name: 1,\n                        created: 1,\n                    }\n\n                    this.app.models.user.find(query, queryOptions).then((users) => {\n                        chanelObject.users = users;\n\n\n                        _.each(memberIds, (id) => {\n\n                            const userId = id.toString();\n                            const memberConnection = this.connections.filter((con) => `${con.userId}` === userId);\n\n                            if (memberConnection.size) {\n                                memberConnection.forEach((con) => {\n\n                                    const ws = con.ws;\n                                    const obj = {\n                                        action: 'channel_added',\n                                        payload: chanelObject,\n                                    }\n\n                                    // send to socket client matching userId in channel members.\n                                    this.send(ws, obj);\n\n                                })\n\n\n                            }\n\n\n                        });\n\n                    });\n\n\n                    //const memberConnections = this.connections.filter((con) => `${con.userId}` = )\n\n\n                });\n\n                //console.log(\"Got new channel need to be created form client\", channel);\n\n\n                break;\n\n            case 'auth':\n\n                const userTokenId = payload;\n                let connection = this.connections.get(socketId);\n\n                if (connection) {\n\n\n\n                    // let find user with this token and verify it.\n\n                    this.app.models.token.loadTokenAndUser(userTokenId).then((token) => {\n\n                        const userId = token.userId;\n\n                        connection.isAuthenticated = true;\n                        connection.userId = `${userId}`;\n\n                        this.connections = this.connections.set(socketId, connection);\n\n                        // now send back to the client you are verified.\n                        const obj = {\n                            action: 'auth_success',\n                            payload: 'You are verified',\n                        }\n                        this.send(connection.ws, obj);\n\n                        //send to all socket clients connection\n\n                        const userIdString = _.toString(userId);\n                        this.sendToMembers(userIdString, {\n                            action: 'user_online',\n                            payload: userIdString,\n                        });\n\n                        this.app.models.user.updateUserStatus(userIdString, true);\n\n\n                    }).catch((err) => {\n\n\n                        // send back to socket client you are not logged.\n                        const obj = {\n                            action: 'auth_error',\n                            payload: \"An error authentication your account: \" + userTokenId\n                        };\n\n                        this.send(connection.ws, obj);\n\n                    })\n\n\n                }\n\n\n                break;\n\n            default:\n\n                break;\n        }\n    }\n\n    modelDidLoad() {\n\n        this.app.wss.on('connection', (ws) => {\n\n            const socketId = new ObjectID().toString();\n\n            //console.log(\"Somone connected to the server via socket.\", socketId)\n\n            const clientConnection = {\n                _id: `${socketId}`,\n                ws: ws,\n                userId: null,\n                isAuthenticated: false,\n            }\n\n            // save this connection client to cache.\n            this.connections = this.connections.set(socketId, clientConnection);\n\n\n            // listen any message from websocket client.\n\n            ws.on('message', (msg) => {\n\n                //console.log(\"SERVER: message from a client\", msg);\n\n                const message = this.decodeMesasge(msg);\n                this.doTheJob(socketId, message);\n\n                //console.log(\"SERVER: message from a client\", msg);\n\n            });\n\n\n            ws.on('close', () => {\n\n                //console.log(\"Someone disconnected to the server\", socketId);\n\n\n                const closeConnection = this.connections.get(socketId);\n                const userId = _.toString(_.get(closeConnection, 'userId', null));\n\n                // let remove this socket client from the cache collection.\n                this.connections = this.connections.remove(socketId);\n\n                if (userId) {\n                    // now find all socket clients matching with userId\n\n                    const userConnections = this.connections.filter((con) => _.toString(_.get(con, 'userId')) === userId);\n                    if (userConnections.size === 0) {\n\n                        // this mean no more socket clients is online with this userId. now user is offline.\n\n                        this.sendToMembers(userId, {\n                            action: 'user_offline',\n                            payload: userId\n                        });\n\n                        // update user status into database\n\n                        this.app.models.user.updateUserStatus(userId, false);\n                    }\n                }\n\n\n            });\n        });\n    }\n}"
  },
  {
    "path": "server/src/models/index.js",
    "content": "import User from './user'\nimport Token from './token'\nimport Connection from './connection'\nimport Channel from './channel'\nimport Message from \"./message\";\n\nexport default class Model{\n\n\tconstructor(app){\n\n\t\tthis.app = app;\n\n\t\tthis.user = new User(app);\n\t\tthis.token = new Token(app);\n\t\tthis.channel = new Channel(app);\n\t\tthis.message = new Message(app);\n\t\tthis.connection = new Connection(app);\n\n\t}\n}"
  },
  {
    "path": "server/src/models/message.js",
    "content": "import _ from 'lodash'\nimport {OrderedMap} from 'immutable'\nimport {ObjectID} from 'mongodb'\n\nexport default class Message {\n\n    constructor(app) {\n        this.app = app;\n        this.messages = new OrderedMap();\n    }\n\n    getChannelMessages(channelId, limit = 50, offset = 0){\n\n        return new Promise((resolve, reject) => {\n\n            channelId = new ObjectID(channelId);\n\n            const query = [\n                {\n\n                    $lookup: {\n                        from: 'users',\n                        localField: 'userId',\n                        foreignField: '_id',\n                        as: 'user'\n                    }\n                },\n                {\n                    $match: {\n                        'channelId': {$eq: channelId},\n                    },\n                },\n                {\n\n                    $project: {\n                        _id: true,\n                        channelId: true,\n                        user: {$arrayElemAt: ['$user', 0]},\n                        userId: true,\n                        body: true,\n                        created: true,\n                    }\n                },\n                {\n                    $project: {\n                        _id: true,\n                        channelId: true,\n                        user: {_id: true, name: true, created: true, online: true},\n                        userId: true,\n                        body: true,\n                        created: true,\n                    }\n                },\n                {\n                    $limit: limit\n                },\n                {\n                    $skip: offset,\n                },\n                {\n                    $sort: {created: -1}\n                }\n\n            ];\n\n\n            this.app.db.collection('messages').aggregate(query, (err, results) => {\n\n                \n\n                return err ? reject(err): resolve(results)\n\n            });\n\n\n        })\n    }\n    create(obj) {\n\n\n        return new Promise((resolve, reject) => {\n\n\n            let id = _.get(obj, '_id', null);\n            id = _.toString(id);\n\n            const userId = new ObjectID(_.get(obj, 'userId'));\n            const channelId = new ObjectID(_.get(obj, 'channelId'));\n\n            const message = {\n                _id: new ObjectID(id),\n                body: _.get(obj, 'body', ''),\n                userId: userId,\n                channelId: channelId,\n                created: new Date(),\n            };\n\n\n            this.app.db.collection('messages').insertOne(message, (err, info) => {\n\n                if(err){\n                    return reject(err);\n                }\n\n\n                // let update lastMessgage field to channel\n                this.app.db.collection('channels').findOneAndUpdate({_id: channelId}, {\n                    $set: {\n                        lastMessage: _.get(message, 'body', ''),\n                        updated: new Date(),\n                    }\n                })\n\n                this.app.models.user.load(_.toString(userId)).then((user) => {\n\n                    _.unset(user, 'password');\n                    _.unset(user, 'email');\n                    message.user = user;\n\n\n                    return resolve(message);\n\n                }).catch((err) => {\n\n                    return reject(err);\n                });\n            });\n\n\n        });\n    }\n\n}"
  },
  {
    "path": "server/src/models/token.js",
    "content": "import _ from 'lodash'\nimport {ObjectID} from 'mongodb'\nimport {OrderedMap} from 'immutable'\n\nexport default class Token{\n\n\tconstructor(app){\n\n\t\tthis.app = app;\n\n\n\t\tthis.tokens = new OrderedMap();\n\n\t}\n\n    logout(token){\n\n\t\treturn new Promise((resolve, reject) => {\n\n            const tokenId = _.toString(token._id);\n            // to remove token from cache\n            this.tokens = this.tokens.remove(tokenId);\n            // we have to delete this token id from tokens collection\n\n            this.app.db.collection('tokens').remove({_id: new ObjectID(tokenId)}, (err, info) => {\n\n\t\t\t\t\treturn err ? reject(err) : resolve(info);\n            });\n\n\t\t})\n\n\t}\n\tloadTokenAndUser(id){\n\n\t\treturn new Promise((resolve, reject) => {\n\n\t\t\tthis.load(id).then((token) => {\n\n\t\t\t\n\t\t\t\tconst userId = `${token.userId}`\n\n\t\t\t\tthis.app.models.user.load(userId).then((user) => {\n\n\t\t\t\t\ttoken.user = user;\n\t\t\t\t\treturn resolve(token);\n\n\t\t\t\t}).catch(err => {\n\n\t\t\t\t\treturn reject(err);\n\n\t\t\t\t});\n\n\n\t\t\t}).catch((err) => {\n\t\t\t\treturn reject(err);\n\t\t\t});\n\n\n\t\t})\n\t}\n\n\tload(id = null){\n\n\n\t\tid = `${id}`;\n\n\n\n\n\t\treturn new Promise((resolve, reject) => {\n\n\n\t\t\t// first we check in cache if found dont need to query to database.\n\n\t\t\tconst tokenFromCache = this.tokens.get(id);\n\t\t\tif(tokenFromCache){\n\n\t\t\t\treturn resolve(tokenFromCache);\n\t\t\t}\n\n\t\t\tthis.findTokenById(id, (err, token) => {\n\n\t\t\t\tif(!err && token){\n\n\t\t\t\t\tconst tokenId = token._id.toString();\n\n\t\t\t\t\tthis.tokens = this.tokens.set(tokenId, token);\n\n\t\t\t\t}\n\t\t\t\treturn err ? reject(err) : resolve(token);\n\n\t\t\t});\n\t\t})\n\t}\n\n\tfindTokenById(id, cb = () => {}){\n\n\n\t\t//console.log(\"Begin query into database!!!!!!\");\n\n\n\t\tconst idObject = new ObjectID(id);\n\t\n\t\tconst query = {_id: idObject}\n\t\tthis.app.db.collection('tokens').findOne(query, (err, result) => {\n\n\t\t\tif(err || !result){\n\n\t\t\t\treturn cb({message: \"Not found\"}, null);\n\t\t\t}\n\n\n\t\t\treturn cb(null, result);\n\n\t\t})\n\t}\n\n\tcreate(userId){\n\n\t\tconst token = {\n\t\t\tuserId: userId,\n\t\t\tcreated: new Date(),\n\t\t}\n\n\t\t\n\t\treturn new Promise((resolve, reject) => {\n\n\n\t\t\tthis.app.db.collection('tokens').insertOne(token, (err, info) => {\n\t\t\t\treturn err ? reject(err) : resolve(token);\n\t\t\t})\n\n\n\n\t\t})\n\t}\n\n\n\n}"
  },
  {
    "path": "server/src/models/user.js",
    "content": "import _ from 'lodash'\nimport {isEmail} from '../helper'\nimport bcrypt from 'bcrypt'\nimport {ObjectID} from 'mongodb'\nimport {OrderedMap} from 'immutable'\n\nconst saltRound = 10;\n\nexport default class User {\n\n    constructor(app) {\n\n        this.app = app;\n\n        this.users = new OrderedMap();\n\n    }\n\n    updateUserStatus(userId, isOnline = false) {\n\n        return new Promise((resolve, reject) => {\n\n            // first update status of cache this.users\n\n\n            this.users = this.users.update(userId, (user) => {\n\n                if (user) {\n                    user.online = isOnline;\n                }\n\n                return user;\n            });\n\n            const query = {_id: new ObjectID(userId)};\n            const updater = {$set: {online: isOnline}};\n            this.app.db.collection('users').update(query, updater, (err, info) => {\n                return err ? reject(err) : resolve(info);\n            });\n\n\n        })\n    }\n\n    find(query = {}, options = {}) {\n\n\n        return new Promise((resolve, reject) => {\n\n            this.app.db.collection('users').find(query, options).toArray((err, users) => {\n\n                return err ? reject(err) : resolve(users);\n            })\n\n        });\n    }\n\n    search(q = \"\") {\n\n        return new Promise((resolve, reject) => {\n\n\n            const regex = new RegExp(q, 'i');\n\n            const query = {\n                $or: [\n                    {name: {$regex: regex}},\n                    {email: {$regex: regex}},\n                ],\n            };\n\n            this.app.db.collection('users').find(query, {\n                _id: true,\n                name: true,\n                created: true\n            }).toArray((err, results) => {\n\n\n                if (err || !results || !results.length) {\n\n                    return reject({message: \"User not found.\"})\n                }\n\n                return resolve(results);\n            });\n\n\n        });\n    }\n\n    login(user) {\n\n        const email = _.get(user, 'email', '');\n        const password = _.get(user, 'password', '');\n\n\n        return new Promise((resolve, reject) => {\n\n\n            if (!password || !email || !isEmail(email)) {\n                return reject({message: \"An error login.\"})\n            }\n\n\n            // find in database with email\n\n            this.findUserByEmail(email, (err, result) => {\n\n\n                if (err) {\n\n                    return reject({message: \"Login Error.\"});\n                }\n\n\n                // if found user we have to compare the password hash and plain text.\n\n\n                const hashPassword = _.get(result, 'password');\n\n                const isMatch = bcrypt.compareSync(password, hashPassword);\n\n\n                if (!isMatch) {\n\n                    return reject({message: \"Login Error.\"});\n                }\n\n                // user login successful let creat new token save to token collection.\n\n                const userId = result._id;\n\n                this.app.models.token.create(userId).then((token) => {\n\n                    token.user = result;\n\n                    return resolve(token);\n\n                }).catch(err => {\n\n                    return reject({message: \"Login error\"});\n                })\n\n\n            });\n\n\n        })\n\n\n    }\n\n    findUserByEmail(email, callback = () => {\n    }) {\n\n\n        this.app.db.collection('users').findOne({email: email}, (err, result) => {\n\n            if (err || !result) {\n\n                return callback({message: \"User not found.\"})\n            }\n\n            return callback(null, result);\n\n        });\n\n\n    }\n\n    load(id) {\n\n\n        id = `${id}`;\n\n        return new Promise((resolve, reject) => {\n\n            // find in cache if found we return and dont nee to query db\n\n            const userInCache = this.users.get(id);\n\n\n            if (userInCache) {\n                return resolve(userInCache);\n            }\n\n            // if not found then we start query db\n            this.findUserById(id, (err, user) => {\n\n                if (!err && user) {\n\n\n                    this.users = this.users.set(id, user);\n                }\n\n                return err ? reject(err) : resolve(user);\n\n            })\n\n\n        })\n    }\n\n    findUserById(id, callback = () => {\n    }) {\n\n        //console.log(\"Begin query in database\");\n\n        if (!id) {\n            return callback({message: \"User not found\"}, null);\n        }\n\n\n        const userId = new ObjectID(id);\n\n        this.app.db.collection('users').findOne({_id: userId}, (err, result) => {\n\n\n            if (err || !result) {\n\n                return callback({message: \"User not found\"});\n            }\n            return callback(null, result);\n\n        });\n    }\n\n    beforeSave(user, callback = () => {\n    }) {\n\n\n        // first is validate user object before save to user collection.\n\n        let errors = [];\n\n\n        const fields = ['name', 'email', 'password'];\n        const validations = {\n            name: {\n                errorMesage: 'Name is required',\n                do: () => {\n\n                    const name = _.get(user, 'name', '');\n\n                    return name.length;\n                }\n            },\n            email: {\n                errorMesage: 'Email is not correct',\n                do: () => {\n\n                    const email = _.get(user, 'email', '');\n\n                    if (!email.length || !isEmail(email)) {\n                        return false;\n                    }\n\n\n                    return true;\n                }\n            },\n            password: {\n                errorMesage: 'Password is required and more than 3 characters',\n                do: () => {\n                    const password = _.get(user, 'password', '');\n\n                    if (!password.length || password.length < 3) {\n\n                        return false;\n                    }\n\n                    return true;\n                }\n            }\n        }\n\n\n        // loop all fields to check if valid or not.\n        fields.forEach((field) => {\n\n\n            const fieldValidation = _.get(validations, field);\n\n            if (fieldValidation) {\n\n                // do check/\n\n                const isValid = fieldValidation.do();\n                const msg = fieldValidation.errorMesage;\n\n                if (!isValid) {\n                    errors.push(msg);\n                }\n            }\n\n\n        });\n\n        if (errors.length) {\n\n            // this is not pass of the validation.\n            const err = _.join(errors, ',');\n            return callback(err, null);\n        }\n\n        // check email is exist in db or not\n        const email = _.toLower(_.trim(_.get(user, 'email', '')));\n\n        this.app.db.collection('users').findOne({email: email}, (err, result) => {\n\n            if (err || result) {\n                return callback({message: \"Email is already exist\"}, null);\n            }\n\n\n            // return callback with succes checked.\n            const password = _.get(user, 'password');\n            const hashPassword = bcrypt.hashSync(password, saltRound);\n\n            const userFormatted = {\n                name: `${_.trim(_.get(user, 'name'))}`,\n                email: email,\n                password: hashPassword,\n                created: new Date(),\n            };\n\n\n            return callback(null, userFormatted);\n\n\n        });\n\n\n    }\n\n    create(user) {\n\n        const db = this.app.db;\n\n        console.log(\"User:\", user)\n\n        return new Promise((resolve, reject) => {\n\n\n            this.beforeSave(user, (err, user) => {\n\n\n                console.log(\"After validation: \", err, user);\n\n\n                if (err) {\n                    return reject(err);\n                }\n\n\n                // insert new user object to users collections\n\n                db.collection('users').insertOne(user, (err, info) => {\n\n\n                    // check if error return error to user\n                    if (err) {\n                        return reject({message: \"An error saving user.\"});\n                    }\n\n                    // otherwise return user object to user.\n\n                    const userId = _.get(user, '_id').toString(); // this is OBJET ID\n\n\n                    this.users = this.users.set(userId, user);\n\n                    return resolve(user);\n\n                });\n\n            });\n\n\n        });\n    }\n}"
  },
  {
    "path": "server/src/www/index.html",
    "content": "<html>\n\n<body>\n\n<h1>Welcome to my website.</h1>\n</body>\n</html>"
  }
]