├── .gitignore ├── README.md ├── client ├── .gitignore ├── README.MD ├── config │ ├── eslint.js │ ├── webpack.config.dev.js │ └── webpack.config.prod.js ├── package.json ├── public │ ├── bundle.js │ └── index.html └── src │ ├── actions │ ├── ActionTypes.js │ ├── message.js │ ├── session.js │ └── ui.js │ ├── components │ ├── Input.js │ ├── Message.js │ ├── MessageList.js │ ├── Sidebar.js │ ├── Spinner.js │ ├── ToggleVideo.js │ ├── VideoScreen.js │ └── index.js │ ├── containers │ ├── App.js │ └── index.js │ ├── index.js │ ├── reducers │ ├── index.js │ ├── message.js │ ├── session.js │ └── ui.js │ ├── sagas │ ├── index.js │ ├── message.js │ └── session.js │ ├── services │ ├── message.js │ └── session.js │ ├── stylesheets │ ├── base │ │ ├── _all.scss │ │ ├── _media.scss │ │ ├── _reset.scss │ │ ├── _typography.scss │ │ └── _variables.scss │ ├── components │ │ ├── _Input.scss │ │ ├── _Message.scss │ │ ├── _MessageList.scss │ │ ├── _Sidebar.scss │ │ ├── _Spinner.scss │ │ ├── _ToggleVideo.scss │ │ ├── _VideoScreen.scss │ │ └── _all.scss │ └── main.scss │ └── utils │ ├── action.js │ └── request.js └── server ├── .env.cpy ├── .gitignore ├── build ├── controllers │ ├── index.js │ ├── message.js │ └── session.js ├── index.js ├── models │ └── message.js ├── routes │ ├── index.js │ ├── message.js │ └── session.js └── utils │ └── MessageCache.js ├── config └── eslint.js ├── package.json ├── scripts └── development.js └── src ├── controllers ├── index.js ├── message.js └── session.js ├── index.js ├── models └── message.js ├── routes ├── index.js ├── message.js └── session.js └── utils └── MessageCache.js /.gitignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velopert/saysomething/d6baaffa5a32e19247b5fab02134be71a5d0ffe5/.gitignore -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SaySomething 2 | 3 | SaySomething is a simple real-time chat application implemented with long polling AJAX technique. React.js is used in front-end side, and Node.js / MongoDB is used in the back-end side. 4 | 5 | **Preview:** https://saysomething.vlpt.us/ 6 | 7 | ##Tech 8 | Following technologies are used in this project: 9 | - react 10 | - webpack 11 | - webpack-dev-server 12 | - babel 13 | - redux 14 | - redux-actions 15 | - redux-saga 16 | - axios 17 | - node-sass 18 | - style-loader, css-loader, sass-loader 19 | - react-custom-scrollbars 20 | - express 21 | - mongodb 22 | - mongoose 23 | - nodemon 24 | - dotenv 25 | - eslint 26 | 27 | ##Getting Started 28 | These instructions will get you a copy of the project up and runing on your local machine for development and testing purposes. 29 | 30 | ###Prerequisites 31 | - node.js 4.5.0^ 32 | - npm 2.15.9^ (using npm 3.x is recommended) 33 | - MongoDB 3.0^ *(or, you can host a databse from https://mlab.com)* 34 | 35 | ###Installing 36 | 1. Install global dependencies 37 | 38 | ``` 39 | npm install -g babel-cli webpack webpack-dev-server nodemon 40 | ``` 41 | - **babel-cli** lets you use babel commands from console. 42 | - **webpack** lets you bundle the client-side codes and **webpack-dev-server** is a module for the development server. 43 | - **nodemon** restarts the node webserver whenever the code changes during development. 44 | 45 | 2. Clone the project from github repository 46 | 47 | ``` 48 | git clone https://github.com/velopert/saysomething.git 49 | cd saysomething 50 | ``` 51 | 52 | 3. Install local dependencies 53 | There are two directores: client / server. Go inside each folder and install the local dependencies. 54 | ``` 55 | cd client 56 | npm install 57 | cd ../server 58 | npm install 59 | ``` 60 | 61 | 62 | 4. Create *server/.env* file 63 | .env file stores the PORT of the server and URI of the mongo database. Rename the .env.cpy file to .env, and fill in the value. If following file is not shown, toggle hidden files visibility. The local dependency *dotenv* loads this file and set the value of process.env.* 64 | ``` 65 | PORT=3000 66 | DB_URI="mongodb://host:port/saysomething" 67 | ``` 68 | 69 | ### Development 70 | In development environment, you have to run dev script from both of the client and server directories (either open multiple terminals or use screen of UNIX) 71 | ``` 72 | cd client 73 | npm run dev 74 | ``` 75 | Development server of client uses 4000 port, and it can be changed from *client/config/webpack.config.dev.js*. Hot-Module-Reloading is enabled in this development server. If you access http://host:4000/ and modify the code, the changes will applied immediately. 76 | ``` 77 | cd server 78 | npm run dev 79 | ``` 80 | When the server runs in development environment, it will restart the server when the files get modified. 81 | 82 | ### Building the source 83 | Both front-end side and back-end side codes need to be built because both sides use ES6 syntax, and additionally, the front-end side uses webpack to bundle the codes. 84 | 85 | To build, you have to run build script in both of the client and server directory as you did when you install the local dependencies. 86 | ``` 87 | cd client 88 | npm run build 89 | ``` 90 | ``` 91 | cd server 92 | npm run build 93 | ``` 94 | 95 | ### Run the server 96 | (You have to go through building process to do this) 97 | ``` 98 | cd server 99 | npm start 100 | ``` 101 | ## License 102 | [MIT License](http://opensource.org/licenses/MIT). 103 | Copyright (c) 2016 [velopert](https://www.velopert.com/). -------------------------------------------------------------------------------- /client/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .eslintrc 3 | -------------------------------------------------------------------------------- /client/README.MD: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/velopert/saysomething/d6baaffa5a32e19247b5fab02134be71a5d0ffe5/client/README.MD -------------------------------------------------------------------------------- /client/config/eslint.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | // Inspired by https://github.com/airbnb/javascript but less opinionated. 11 | 12 | // We use eslint-loader so even warnings are very visibile. 13 | // This is why we only use "WARNING" level for potential errors, 14 | // and we don't use "ERROR" level at all. 15 | 16 | // In the future, we might create a separate list of rules for production. 17 | // It would probably be more strict. 18 | 19 | module.exports = { 20 | root: true, 21 | 22 | parser: 'babel-eslint', 23 | 24 | // import plugin is termporarily disabled, scroll below to see why 25 | plugins: [/*'import', */'flowtype', 'jsx-a11y', 'react'], 26 | 27 | env: { 28 | browser: true, 29 | commonjs: true, 30 | es6: true, 31 | node: true 32 | }, 33 | 34 | parserOptions: { 35 | ecmaVersion: 6, 36 | sourceType: 'module', 37 | ecmaFeatures: { 38 | jsx: true, 39 | generators: true, 40 | experimentalObjectRestSpread: true 41 | } 42 | }, 43 | 44 | settings: { 45 | 'import/ignore': [ 46 | 'node_modules', 47 | '\\.(json|css|jpg|png|gif|eot|svg|ttf|woff|woff2|mp4|webm)$', 48 | ], 49 | 'import/extensions': ['.js'], 50 | 'import/resolver': { 51 | node: { 52 | extensions: ['.js', '.json'] 53 | } 54 | } 55 | }, 56 | 57 | rules: { 58 | // http://eslint.org/docs/rules/ 59 | 'array-callback-return': 'warn', 60 | 'default-case': ['warn', { commentPattern: '^no default$' }], 61 | 'dot-location': ['warn', 'property'], 62 | eqeqeq: ['warn', 'allow-null'], 63 | 'guard-for-in': 'warn', 64 | 'new-cap': ['warn', { newIsCap: true }], 65 | 'new-parens': 'warn', 66 | 'no-array-constructor': 'warn', 67 | 'no-caller': 'warn', 68 | 'no-cond-assign': ['warn', 'always'], 69 | 'no-const-assign': 'warn', 70 | 'no-control-regex': 'warn', 71 | 'no-delete-var': 'warn', 72 | 'no-dupe-args': 'warn', 73 | 'no-dupe-class-members': 'warn', 74 | 'no-dupe-keys': 'warn', 75 | 'no-duplicate-case': 'warn', 76 | 'no-empty-character-class': 'warn', 77 | 'no-empty-pattern': 'warn', 78 | 'no-eval': 'warn', 79 | 'no-ex-assign': 'warn', 80 | 'no-extend-native': 'warn', 81 | 'no-extra-bind': 'warn', 82 | 'no-extra-label': 'warn', 83 | 'no-fallthrough': 'warn', 84 | 'no-func-assign': 'warn', 85 | 'no-implied-eval': 'warn', 86 | 'no-invalid-regexp': 'warn', 87 | 'no-iterator': 'warn', 88 | 'no-label-var': 'warn', 89 | 'no-labels': ['warn', { allowLoop: false, allowSwitch: false }], 90 | 'no-lone-blocks': 'warn', 91 | 'no-loop-func': 'warn', 92 | 'no-mixed-operators': ['warn', { 93 | groups: [ 94 | ['&', '|', '^', '~', '<<', '>>', '>>>'], 95 | ['==', '!=', '===', '!==', '>', '>=', '<', '<='], 96 | ['&&', '||'], 97 | ['in', 'instanceof'] 98 | ], 99 | allowSamePrecedence: false 100 | }], 101 | 'no-multi-str': 'warn', 102 | 'no-native-reassign': 'warn', 103 | 'no-negated-in-lhs': 'warn', 104 | 'no-new-func': 'warn', 105 | 'no-new-object': 'warn', 106 | 'no-new-symbol': 'warn', 107 | 'no-new-wrappers': 'warn', 108 | 'no-obj-calls': 'warn', 109 | 'no-octal': 'warn', 110 | 'no-octal-escape': 'warn', 111 | 'no-redeclare': 'warn', 112 | 'no-regex-spaces': 'warn', 113 | 'no-restricted-syntax': [ 114 | 'warn', 115 | 'LabeledStatement', 116 | 'WithStatement', 117 | ], 118 | 'no-return-assign': 'warn', 119 | 'no-script-url': 'warn', 120 | 'no-self-assign': 'warn', 121 | 'no-self-compare': 'warn', 122 | 'no-sequences': 'warn', 123 | 'no-shadow-restricted-names': 'warn', 124 | 'no-sparse-arrays': 'warn', 125 | 'no-this-before-super': 'warn', 126 | 'no-throw-literal': 'warn', 127 | 'no-undef': 'warn', 128 | 'no-unexpected-multiline': 'warn', 129 | 'no-unreachable': 'warn', 130 | 'no-unused-expressions': 'warn', 131 | 'no-unused-labels': 'warn', 132 | 'no-unused-vars': ['warn', { vars: 'local', args: 'none' }], 133 | 'no-use-before-define': ['warn', 'nofunc'], 134 | 'no-useless-computed-key': 'warn', 135 | 'no-useless-concat': 'warn', 136 | 'no-useless-constructor': 'warn', 137 | 'no-useless-escape': 'warn', 138 | 'no-useless-rename': ['warn', { 139 | ignoreDestructuring: false, 140 | ignoreImport: false, 141 | ignoreExport: false, 142 | }], 143 | 'no-with': 'warn', 144 | 'no-whitespace-before-property': 'warn', 145 | 'operator-assignment': ['warn', 'always'], 146 | radix: 'warn', 147 | 'require-yield': 'warn', 148 | 'rest-spread-spacing': ['warn', 'never'], 149 | strict: ['warn', 'never'], 150 | 'unicode-bom': ['warn', 'never'], 151 | 'use-isnan': 'warn', 152 | 'valid-typeof': 'warn', 153 | 154 | // https://github.com/benmosher/eslint-plugin-import/blob/master/docs/rules/ 155 | 156 | // TODO: import rules are temporarily disabled because they don't play well 157 | // with how eslint-loader only checks the file you change. So if module A 158 | // imports module B, and B is missing a default export, the linter will 159 | // record this as an issue in module A. Now if you fix module B, the linter 160 | // will not be aware that it needs to re-lint A as well, so the error 161 | // will stay until the next restart, which is really confusing. 162 | 163 | // This is probably fixable with a patch to eslint-loader. 164 | // When file A is saved, we want to invalidate all files that import it 165 | // *and* that currently have lint errors. This should fix the problem. 166 | 167 | // 'import/default': 'warn', 168 | // 'import/export': 'warn', 169 | // 'import/named': 'warn', 170 | // 'import/namespace': 'warn', 171 | // 'import/no-amd': 'warn', 172 | // 'import/no-duplicates': 'warn', 173 | // 'import/no-extraneous-dependencies': 'warn', 174 | // 'import/no-named-as-default': 'warn', 175 | // 'import/no-named-as-default-member': 'warn', 176 | // 'import/no-unresolved': ['warn', { commonjs: true }], 177 | 178 | // https://github.com/yannickcr/eslint-plugin-react/tree/master/docs/rules 179 | 'react/jsx-equals-spacing': ['warn', 'never'], 180 | 'react/jsx-no-duplicate-props': ['warn', { ignoreCase: true }], 181 | 'react/jsx-no-undef': 'warn', 182 | 'react/jsx-pascal-case': ['warn', { 183 | allowAllCaps: true, 184 | ignore: [], 185 | }], 186 | 'react/jsx-uses-react': 'warn', 187 | 'react/jsx-uses-vars': 'warn', 188 | 'react/no-deprecated': 'warn', 189 | 'react/no-direct-mutation-state': 'warn', 190 | 'react/no-is-mounted': 'warn', 191 | 'react/react-in-jsx-scope': 'warn', 192 | 'react/require-render-return': 'warn', 193 | 194 | // https://github.com/evcohen/eslint-plugin-jsx-a11y/tree/master/docs/rules 195 | 'jsx-a11y/aria-role': 'warn', 196 | 'jsx-a11y/img-has-alt': 'warn', 197 | 'jsx-a11y/img-redundant-alt': 'warn', 198 | 'jsx-a11y/no-access-key': 'warn', 199 | 200 | // https://github.com/gajus/eslint-plugin-flowtype 201 | 'flowtype/define-flow-type': 'warn', 202 | 'flowtype/require-valid-file-annotation': 'warn', 203 | 'flowtype/use-flow-type': 'warn' 204 | } 205 | }; 206 | -------------------------------------------------------------------------------- /client/config/webpack.config.dev.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | entry: ['babel-polyfill', './src/index.js', './src/stylesheets/main.scss'], 6 | 7 | output: { 8 | path: '/', 9 | filename: 'bundle.js' 10 | }, 11 | 12 | devServer: { 13 | hot: true, 14 | inline: true, 15 | host: '0.0.0.0', 16 | port: 4000, 17 | contentBase: __dirname + '/public/', 18 | proxy: { 19 | "*": "http://localhost:3000" // express 서버주소 20 | } 21 | }, 22 | 23 | module: { 24 | loaders: [ 25 | { 26 | test: /\.js$/, 27 | loaders: ['react-hot', 'babel?' + JSON.stringify({ 28 | cacheDirectory: true, 29 | presets: ['es2015', 'react', 'stage-0'] 30 | })], 31 | exclude: /node_modules/, 32 | }, 33 | { 34 | test: /\.scss$/, 35 | loaders: ["style", "css", "sass"] 36 | } 37 | ] 38 | }, 39 | 40 | plugins: [ 41 | new webpack.HotModuleReplacementPlugin() 42 | ], 43 | 44 | resolve: { 45 | root: path.resolve('./src') 46 | } 47 | }; 48 | -------------------------------------------------------------------------------- /client/config/webpack.config.prod.js: -------------------------------------------------------------------------------- 1 | var webpack = require('webpack'); 2 | var path = require('path'); 3 | 4 | module.exports = { 5 | entry: ['babel-polyfill', './src/index.js', './src/stylesheets/main.scss'], 6 | 7 | output: { 8 | path: __dirname + '/../public/', 9 | filename: 'bundle.js' 10 | }, 11 | 12 | module: { 13 | loaders: [ 14 | { 15 | test: /\.js$/, 16 | loaders: ['babel?' + JSON.stringify({ 17 | cacheDirectory: true, 18 | presets: ['es2015', 'react', 'stage-0'] 19 | })], 20 | exclude: /node_modules/, 21 | }, 22 | { 23 | test: /\.scss$/, 24 | loaders: ["style", "css", "sass"] 25 | } 26 | ] 27 | }, 28 | 29 | plugins: [ 30 | new webpack.DefinePlugin({ 'process.env.NODE_ENV': '"production"' }), 31 | new webpack.optimize.OccurrenceOrderPlugin(), 32 | new webpack.optimize.DedupePlugin(), 33 | new webpack.optimize.UglifyJsPlugin({ 34 | compress: { 35 | screw_ie8: true, 36 | warnings: false 37 | }, 38 | mangle: { 39 | screw_ie8: true 40 | }, 41 | output: { 42 | comments: false, 43 | screw_ie8: true 44 | } 45 | }) 46 | ], 47 | 48 | resolve: { 49 | root: path.resolve('./src') 50 | } 51 | }; 52 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saysomething-front-end", 3 | "version": "1.0.0", 4 | "main": "index.js", 5 | "scripts": { 6 | "dev": "webpack-dev-server --config ./config/webpack.config.dev.js", 7 | "build": "webpack --config ./config/webpack.config.prod.js" 8 | }, 9 | "author": "", 10 | "license": "ISC", 11 | "description": "", 12 | "dependencies": { 13 | "axios": "^0.13.1", 14 | "babel-polyfill": "^6.13.0", 15 | "babel-preset-stage-0": "^6.5.0", 16 | "css-loader": "^0.23.1", 17 | "node-sass": "^3.8.0", 18 | "react": "^15.2.1", 19 | "react-custom-scrollbars": "^4.0.0", 20 | "react-dom": "^15.2.1", 21 | "react-redux": "^4.4.5", 22 | "redux": "^3.5.2", 23 | "redux-actions": "^0.11.0", 24 | "redux-logger": "^2.6.1", 25 | "redux-saga": "^0.11.0", 26 | "sass-loader": "^4.0.0", 27 | "style-loader": "^0.13.1" 28 | }, 29 | "devDependencies": { 30 | "babel-core": "^6.9.1", 31 | "babel-loader": "^6.2.4", 32 | "babel-preset-es2015": "^6.9.0", 33 | "babel-preset-react": "^6.5.0", 34 | "react-hot-loader": "^1.3.0", 35 | "webpack": "^1.13.1", 36 | "webpack-dev-server": "^1.14.1" 37 | }, 38 | "eslintConfig": { 39 | "extends": "./config/eslint.js" 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /client/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | SaySomething 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /client/src/actions/ActionTypes.js: -------------------------------------------------------------------------------- 1 | import createRequestTypes from 'utils/action'; 2 | 3 | 4 | /* MESSAGE */ 5 | export const FETCH_MESSAGE = createRequestTypes('FETCH_MESSAGE'); 6 | export const WRITE_MESSAGE = createRequestTypes('WRITE_MESSAGE'); 7 | export const TOGGLE_LOADING = "TOGGLE_LOADING"; 8 | 9 | /* SESSION */ 10 | export const GET_SESSION = createRequestTypes('GET_SESSION'); 11 | 12 | 13 | /* UI */ 14 | export const CHANGE_MESSAGE_INPUT = "CHANGE_MESSAGE_INPUT"; 15 | export const TOGGLE_VIDEO = "TOGGLE_VIDEO"; 16 | -------------------------------------------------------------------------------- /client/src/actions/message.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './ActionTypes'; 2 | import { createAction } from 'redux-actions'; 3 | 4 | export const fetchMessage = createAction(ActionTypes.FETCH_MESSAGE.REQUEST); 5 | export const writeMessage = createAction(ActionTypes.WRITE_MESSAGE.REQUEST); 6 | export const toggleLoading = createAction(ActionTypes.TOGGLE_LOADING); 7 | -------------------------------------------------------------------------------- /client/src/actions/session.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './ActionTypes'; 2 | import { createAction } from 'redux-actions'; 3 | 4 | export const getSession = createAction(ActionTypes.GET_SESSION.REQUEST); 5 | -------------------------------------------------------------------------------- /client/src/actions/ui.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from './ActionTypes'; 2 | import { createAction } from 'redux-actions'; 3 | 4 | export const changeMessageInput = createAction(ActionTypes.CHANGE_MESSAGE_INPUT); 5 | export const toggleVideo = createAction(ActionTypes.TOGGLE_VIDEO); 6 | -------------------------------------------------------------------------------- /client/src/components/Input.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | value: PropTypes.string, 5 | change: PropTypes.func, 6 | onWrite: PropTypes.func 7 | }; 8 | 9 | const defaultProps = { 10 | value: '', 11 | change: () => { console.error('onChange not defined'); }, 12 | onWrite: () => { console.error('onWrite not defined'); } 13 | }; 14 | 15 | class Input extends PureComponent { 16 | 17 | constructor(props) { 18 | super(props); 19 | this.handleChange = this.handleChange.bind(this); 20 | this.handleKeyPress = this.handleKeyPress.bind(this); 21 | } 22 | 23 | componentDidMount() { 24 | this.input.focus(); 25 | } 26 | 27 | handleChange(e) { 28 | if(e.target.value.length < 50) 29 | this.props.change(e.target.value); 30 | } 31 | 32 | handleKeyPress(e) { 33 | if(e.charCode === 13) { 34 | this.props.onWrite(); 35 | this.props.change(''); 36 | } 37 | } 38 | 39 | render() { 40 | return( 41 |
42 | {this.input = ref}}/> 49 |
50 | ); 51 | } 52 | } 53 | 54 | Input.propTypes = propTypes; 55 | Input.defaultProps = defaultProps; 56 | 57 | export default Input; 58 | -------------------------------------------------------------------------------- /client/src/components/Message.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | data: PropTypes.object 5 | }; 6 | 7 | const defaultProps = { 8 | data: { 9 | "_id": "57b827c05b8da9d8388d8ee9", 10 | "message": "Hi", 11 | "uid": "is2w2iowjw9", 12 | "__v": 0, 13 | "color": [ 14 | 0, 15 | 231, 16 | 155 17 | ], 18 | "date": "2016-08-20T09:49:52.405Z" 19 | } 20 | }; 21 | 22 | class Message extends PureComponent { 23 | 24 | constructor(props) { 25 | super(props); 26 | } 27 | 28 | render() { 29 | const { message, color, _id } = this.props.data; 30 | const opacity = (_id==="") ? 0.27 : 0.35; 31 | 32 | const style = { 33 | backgroundColor: `rgba(${color[0]},${color[1]},${color[2]},${opacity})` 34 | } 35 | 36 | return( 37 |
38 | {message} 39 |
40 | ); 41 | } 42 | } 43 | 44 | Message.propTypes = propTypes; 45 | Message.defaultProps = defaultProps; 46 | 47 | export default Message; 48 | -------------------------------------------------------------------------------- /client/src/components/MessageList.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes } from 'react'; 2 | import { Message, Spinner } from 'components'; 3 | import { Scrollbars } from 'react-custom-scrollbars'; 4 | 5 | const propTypes = { 6 | data: PropTypes.array, 7 | tempData: PropTypes.array, 8 | loadingHistory: PropTypes.bool, 9 | toggleLoading: PropTypes.func, 10 | isAtTop: PropTypes.bool, 11 | fetchHistory: PropTypes.func 12 | }; 13 | 14 | const defaultProps = { 15 | data: [], 16 | tempData: [], 17 | loadingHistory: false, 18 | toggleLoading: () => { console.error('toggleLoading not defined'); }, 19 | isAtTop: false, 20 | fetchHistory: () => { console.error('fetchHistory not defined');} 21 | }; 22 | 23 | class MessageList extends PureComponent { 24 | 25 | constructor(props) { 26 | super(props); 27 | this.handleScroll = this.handleScroll.bind(this); 28 | this.scrollToBottom = this.scrollToBottom.bind(this); 29 | this.state = { 30 | initScrolled: false, 31 | previousHeight: 0 32 | }; 33 | } 34 | 35 | componentDidUpdate(prevProps, prevState) { 36 | 37 | // THIS PART HANDLES AUTO SCROLLING AFTER DATA FETCHING 38 | if(prevProps.data.length !== this.props.data.length) { 39 | 40 | if(this.props.fetchedHistory) { 41 | // IF LOADES OLDER MESSAGE, SET THE SCROLL SO SO THAT THE USER 42 | // CAN READ THE SAME PART THAT HE/SHE WAS LOOKING AT 43 | this.element.scrollTop(this.element.getScrollHeight() - this.state.previousHeight); 44 | } 45 | 46 | 47 | // IF THE SCROLLBAR IS CLOSE TO THE SCROLLBOTTOM, SCROLL TO BOTTOM 48 | // IF IT IS A INITIAL FETCHING, FIRST STATEMENT IS NOT TRUE, SO USE A initScrolled STATE TO HANDLE THIS 49 | if(this.element.getScrollHeight() - this.element.refs.view.clientHeight - this.element.getScrollTop() < 200 || !this.state.initScrolled) { 50 | this.scrollToBottom(); 51 | 52 | if(!this.state.initScrolled) { 53 | this.setState({ 54 | initScrolled: true 55 | }); 56 | } 57 | } 58 | 59 | 60 | // SAVES THE PREVIOUS SCROLL POS 61 | if(this.state.previousHeight !== this.element.getScrollHeight()) { 62 | this.setState({previousHeight: this.element.getScrollHeight()}); 63 | } 64 | 65 | 66 | } 67 | 68 | if(prevProps.tempData.length < this.props.tempData.length) { 69 | this.scrollToBottom(); 70 | }; 71 | } 72 | 73 | handleScroll(e) { 74 | // THIS PART HANDLES THE MESSAGE HISTORY FETCHING 75 | if(this.element.getScrollTop() <= 60 && !this.props.loadingHistory 76 | && this.props.data.length >= 25 && !this.props.isAtTop) { 77 | this.props.fetchHistory(); 78 | this.props.toggleLoading(); 79 | } 80 | } 81 | 82 | renderThumb({ style, ...props }) { 83 | // IT STYLIZES THE CUSTOM SCROLLBAR 84 | const thumbStyle = { 85 | backgroundColor: 'rgba(255,255, 255, 0.8)', 86 | borderRadius: '3px' 87 | }; 88 | 89 | return ( 90 |
93 | ); 94 | } 95 | 96 | scrollToBottom() { 97 | // SCROLL TO BOTTOM 98 | this.element.scrollTop(this.element.getScrollHeight()); 99 | } 100 | 101 | 102 | mapDataToMessages(data) { 103 | // MAP DATA TO MESSAGE COMPONENTS 104 | return data.map( 105 | (message) => { 106 | return ( 107 | 108 | ); 109 | } 110 | ) 111 | } 112 | 113 | render() { 114 | 115 | // SPINNER IS VISIBLE ONLY WHEN THE USER IS NOT READING THE FIRST PAGE 116 | 117 | const spinnerVisibility = (this.props.data.length >= 25 && !this.props.isAtTop); 118 | 119 | return( 120 |
121 | {this.element = ref}} 124 | onScroll={this.handleScroll}> 125 | 126 | {this.mapDataToMessages(this.props.data)} 127 | {this.mapDataToMessages(this.props.tempData)} 128 | 129 |
130 | ); 131 | } 132 | } 133 | 134 | MessageList.propTypes = propTypes; 135 | MessageList.defaultProps = defaultProps; 136 | 137 | export default MessageList; 138 | -------------------------------------------------------------------------------- /client/src/components/Sidebar.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes } from 'react'; 2 | 3 | class Sidebar extends PureComponent { 4 | render() { 5 | return( 6 |
7 |
SaySomething.
8 | {this.props.children} 9 |
10 | ); 11 | } 12 | } 13 | 14 | 15 | export default Sidebar; 16 | -------------------------------------------------------------------------------- /client/src/components/Spinner.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | visible: PropTypes.bool 5 | }; 6 | 7 | const defaultProps = { 8 | visible: true 9 | }; 10 | 11 | class Spinner extends PureComponent { 12 | 13 | render() { 14 | 15 | const spinner = ( 16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 | ); 31 | return( 32 |
33 | {this.props.visible ? spinner : undefined} 34 |
35 | ); 36 | } 37 | } 38 | 39 | Spinner.propTypes = propTypes; 40 | Spinner.defaultProps = defaultProps; 41 | 42 | export default Spinner; 43 | -------------------------------------------------------------------------------- /client/src/components/ToggleVideo.js: -------------------------------------------------------------------------------- 1 | import React, { PureComponent, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | value: PropTypes.bool, 5 | onToggle: PropTypes.func 6 | }; 7 | 8 | const defaultProps = { 9 | value: true, 10 | onToggle: () => { console.error('onToggle not defined'); } 11 | }; 12 | 13 | class ToggleVideo extends PureComponent { 14 | 15 | render() { 16 | const text = this.props.value ? "HIDE VIDEO" : "SHOW VIDEO"; 17 | 18 | return( 19 |
{text}
21 | ); 22 | } 23 | } 24 | 25 | ToggleVideo.propTypes = propTypes; 26 | ToggleVideo.defaultProps = defaultProps; 27 | 28 | export default ToggleVideo; 29 | -------------------------------------------------------------------------------- /client/src/components/VideoScreen.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | 3 | const propTypes = { 4 | videoUrl: PropTypes.string, 5 | visibility: PropTypes.bool 6 | }; 7 | 8 | const defaultProps = { 9 | videoUrl: "https://www.youtube.com/embed/njCDZWTI-xg?controls=1&showinfo=0&rel=0&autoplay=1&loop=1&playlist=njCDZWTI-xg", 10 | visibility: true 11 | }; 12 | 13 | class VideoScreen extends Component { 14 | render() { 15 | return ( 16 |
17 |
18 | {this.props.visibility ? : undefined } 19 |
20 |
21 | ); 22 | } 23 | } 24 | 25 | VideoScreen.propTypes = propTypes; 26 | VideoScreen.defaultProps = defaultProps; 27 | 28 | export default VideoScreen; 29 | -------------------------------------------------------------------------------- /client/src/components/index.js: -------------------------------------------------------------------------------- 1 | import VideoScreen from './VideoScreen'; 2 | import Sidebar from './Sidebar'; 3 | import Input from './Input'; 4 | import MessageList from './MessageList'; 5 | import Message from './Message'; 6 | import Spinner from './Spinner'; 7 | import ToggleVideo from './ToggleVideo'; 8 | export { VideoScreen, Sidebar, Input, MessageList, Message, Spinner, ToggleVideo}; 9 | -------------------------------------------------------------------------------- /client/src/containers/App.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { VideoScreen, Sidebar, Input, MessageList, ToggleVideo } from 'components'; 3 | import { connect } from 'react-redux'; 4 | import { fetchMessage, writeMessage, toggleLoading } from 'actions/message'; 5 | import { getSession } from 'actions/session'; 6 | import * as uiActions from 'actions/ui'; 7 | 8 | class App extends Component { 9 | 10 | componentDidMount() { 11 | 12 | // GET THE SESSION 13 | this.props.sessionEvents.get(); 14 | // FETCH THE INTIAL DATA 15 | this.props.msgEvents.fetch({initial: true }); 16 | 17 | // BIND THE METHODS 18 | this.handleWrite = this.handleWrite.bind(this); 19 | this.fetchHistory = this.fetchHistory.bind(this); 20 | 21 | } 22 | 23 | handleWrite() { 24 | if(this.props.ui.Input.value.length === 0) return; 25 | 26 | function generateUID() { 27 | return (new Date().valueOf()).toString(36) 28 | + ("000" + (Math.random() * Math.pow(36,3) << 0).toString(36)).slice(-3); 29 | } 30 | 31 | this.props.msgEvents.write({message: this.props.ui.Input.value, uid: generateUID(), session: this.props.session, scroll: this.MessageList.scrollToBottom}); 32 | } 33 | 34 | 35 | fetchHistory() { 36 | // WHEN LOADING OLDER DATA, THE PIVOT IS THE FIRST ITEM SHOWN IN THE LIST 37 | this.props.msgEvents.fetch({initial: false, latest: false, pivot: this.props.message.data[0]._id}); 38 | } 39 | 40 | render(){ 41 | return ( 42 |
43 | 45 | 47 | 48 | {this.MessageList = ref;}} 51 | isAtTop={this.props.message.isAtTop} 52 | toggleLoading={this.props.msgEvents.toggleLoading} 53 | loadingHistory={this.props.message.loadingHistory} 54 | fetchHistory={this.fetchHistory} 55 | fetchedHistory={this.props.message.fetchedHistory}/> 56 | 60 | 61 |
62 | ); 63 | } 64 | } 65 | 66 | function mapStateToProps(state) { 67 | return { 68 | ui: state.ui, 69 | message: state.message, 70 | session: state.session.session 71 | }; 72 | } 73 | 74 | function mapDispatchToProps(dispatch) { 75 | return { 76 | uiEvents: { 77 | Input: { 78 | change: (payload) => dispatch(uiActions.changeMessageInput(payload)) 79 | }, 80 | Video: { 81 | toggle: () => dispatch(uiActions.toggleVideo()) 82 | } 83 | }, 84 | sessionEvents: { 85 | get: () => dispatch(getSession()) 86 | }, 87 | msgEvents: { 88 | fetch: (payload) => dispatch(fetchMessage(payload)), 89 | write: (payload) => dispatch(writeMessage(payload)), 90 | toggleLoading: () => dispatch(toggleLoading()) 91 | } 92 | }; 93 | } 94 | 95 | export default connect(mapStateToProps, mapDispatchToProps)(App); 96 | -------------------------------------------------------------------------------- /client/src/containers/index.js: -------------------------------------------------------------------------------- 1 | import App from './App'; 2 | 3 | export { App }; 4 | -------------------------------------------------------------------------------- /client/src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import { App } from 'containers'; 4 | 5 | import { createStore, applyMiddleware } from 'redux'; 6 | import { Provider } from 'react-redux'; 7 | import createSagaMiddleware from 'redux-saga' 8 | import reducers from 'reducers'; 9 | import rootSaga from 'sagas'; 10 | 11 | 12 | const sagaMiddleware = createSagaMiddleware(); 13 | 14 | 15 | let middleware = [sagaMiddleware]; 16 | 17 | 18 | // DO NOT USE REDUX-LOGGER IN PRODUCTION ENV 19 | if (process.env.NODE_ENV !== 'production') { 20 | const createLogger = require('redux-logger'); 21 | const logger = createLogger(); 22 | middleware = [...middleware, logger]; 23 | } 24 | 25 | const store = createStore( 26 | reducers, 27 | applyMiddleware(...middleware) 28 | ); 29 | 30 | sagaMiddleware.run(rootSaga); 31 | 32 | const rootElement = document.getElementById('root'); 33 | ReactDOM.render( 34 | 35 | 36 | 37 | , rootElement); 38 | -------------------------------------------------------------------------------- /client/src/reducers/index.js: -------------------------------------------------------------------------------- 1 | import { combineReducers } from 'redux'; 2 | import message from './message'; 3 | import ui from './ui'; 4 | import session from './session'; 5 | 6 | export default combineReducers({message, ui, session}); 7 | -------------------------------------------------------------------------------- /client/src/reducers/message.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from 'actions/ActionTypes'; 2 | 3 | const request = { 4 | fetching: false, 5 | fetched: false, 6 | response: null, 7 | error: null 8 | } 9 | 10 | const initialState = { 11 | data: [], 12 | tempData: [], 13 | isAtTop: false, 14 | loadingHistory: false, 15 | fetchedHistory: false, 16 | requests: { 17 | fetch: { ...request }, 18 | write: { ...request } 19 | } 20 | } 21 | 22 | function message(state = initialState, action) { 23 | switch(action.type) { 24 | case ActionTypes.FETCH_MESSAGE.REQUEST: 25 | // FETCHING JUST HAS BEGUN 26 | return { 27 | ...state, 28 | requests: { 29 | ...state.requests, 30 | fetch: { 31 | ...state.requests.fetch, 32 | fetching: true, 33 | fetched: false, 34 | response: null, 35 | error: null 36 | } 37 | } 38 | }; 39 | case ActionTypes.FETCH_MESSAGE.SUCCESS: 40 | if(action.payload.initial) { 41 | // INITIAL FETCH 42 | return { 43 | ...state, 44 | fetchedHistory: false, 45 | data: action.payload.response.data, 46 | requests: { 47 | ...state.requests, 48 | fetch: { 49 | ...state.requests.fetch, 50 | fetching: false, 51 | fetched: true, 52 | } 53 | } 54 | }; 55 | } else { 56 | // RECENT FETCH 57 | if(action.payload.latest) { 58 | if(state.tempData.length ===0) { 59 | return { 60 | ...state, 61 | fetchedHistory: false, 62 | data: [...state.data, ...action.payload.response.data], 63 | requests: { 64 | ...state.requests, 65 | fetch: { 66 | ...state.requests.fetch, 67 | fetching: false, 68 | fetched: true, 69 | } 70 | } 71 | }; 72 | } else { 73 | // USER HAD WRITTEN NEW MESSAGES 74 | // COPY THE DATA FIRST 75 | let newData = [ ...action.payload.response.data]; 76 | let tempData = [ ...state.tempData]; 77 | 78 | // IF THE tempData ITEM IS FOUND IN newData, REMOVE IT 79 | for(let i = 0; i < newData.length; i++) { 80 | for(let j = 0; j < tempData.length; j++) { 81 | if(newData[i].uid === tempData[j].uid) { 82 | tempData.splice(j,1); 83 | } 84 | } 85 | } 86 | 87 | return { 88 | ...state, 89 | fetchedHistory: false, 90 | data: [...state.data, ...action.payload.response.data], 91 | tempData: tempData, 92 | requests: { 93 | ...state.requests, 94 | fetch: { 95 | ...state.requests.fetch, 96 | fetching: false, 97 | fetched: true, 98 | } 99 | } 100 | }; 101 | } 102 | } else { 103 | // LOAD OLD MESSAGES 104 | return { 105 | ...state, 106 | fetchedHistory: true, 107 | /* IF THE NUMBER OF THE ITEMS JUST HAS LOADED IS LESS THAN 25 108 | THAT MEANS THE USER IS READING THE FIRST PAGE */ 109 | isAtTop: (action.payload.response.data.length < 25), 110 | loadingHistory: false, 111 | data: [...action.payload.response.data, ...state.data], 112 | requests: { 113 | ...state.requests, 114 | fetch: { 115 | ...state.requests.fetch, 116 | fetching: false, 117 | fetched: true, 118 | } 119 | } 120 | }; 121 | } 122 | } 123 | case ActionTypes.FETCH_MESSAGE.FAILURE: 124 | // FETCHING FAILED 125 | return { 126 | ...state, 127 | requests: { 128 | ...state.requests, 129 | fetch: { 130 | ...state.requests.fetch, 131 | fetching: false, 132 | error: action.payload 133 | } 134 | } 135 | } 136 | case ActionTypes.WRITE_MESSAGE.REQUEST: 137 | /* WRITING MESSAGE HAS BEGUN 138 | CREATE A TEMP MESSAGE AND STORE IN TEMPDATA 139 | THIS IS TO SHOW THE MESSAGE DIRECTLY ON THE SCREEN 140 | EVEN IF IT IS NOT ADDED TO SERVER DATABASE, YET */ 141 | const tempMessage = { 142 | _id: '', 143 | message: action.payload.message, 144 | uid: action.payload.uid, 145 | color: action.payload.session.color 146 | }; 147 | 148 | return { 149 | ...state, 150 | tempData: [...state.tempData, tempMessage] 151 | }; 152 | case ActionTypes.TOGGLE_LOADING: 153 | /* 154 | TELL THE SYSTEM THAT THE USER IS LOADING THE HISTORY ALREADY 155 | SO THAT IT DOES NOT FETCH DUPLICATELY 156 | */ 157 | return { 158 | ...state, 159 | loadingHistory: !state.loadingHistory 160 | }; 161 | default: 162 | return state; 163 | } 164 | } 165 | 166 | export default message; 167 | -------------------------------------------------------------------------------- /client/src/reducers/session.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from 'actions/ActionTypes'; 2 | 3 | const initialState = { 4 | session: null 5 | }; 6 | 7 | export default function ui(state = initialState, action) { 8 | switch(action.type) { 9 | case ActionTypes.GET_SESSION.SUCCESS: 10 | // GETS THE SESSION AND STORE IN THE STATE 11 | return { 12 | session: action.payload.response.data 13 | }; 14 | default: 15 | return state; 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /client/src/reducers/ui.js: -------------------------------------------------------------------------------- 1 | import * as ActionTypes from 'actions/ActionTypes'; 2 | 3 | const initialState = { 4 | Input: { 5 | value: '' 6 | }, 7 | Video: { 8 | visibility: true 9 | } 10 | } 11 | 12 | export default function ui(state = initialState, action) { 13 | switch(action.type) { 14 | case ActionTypes.CHANGE_MESSAGE_INPUT: 15 | // CHANGE THE MESSAGE INPUT 16 | return { 17 | ...state, 18 | Input: { 19 | value: action.payload 20 | } 21 | }; 22 | case ActionTypes.TOGGLE_VIDEO: 23 | // TOGGLE THE VIDEO STATE 24 | return { 25 | ...state, 26 | Video: { 27 | visibility: !state.Video.visibility 28 | } 29 | }; 30 | default: 31 | return state; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /client/src/sagas/index.js: -------------------------------------------------------------------------------- 1 | import message from './message'; 2 | import session from './session'; 3 | 4 | // LOAD THE SAGA 5 | export default function* rootSaga() { 6 | yield [ 7 | message(), 8 | session() 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /client/src/sagas/message.js: -------------------------------------------------------------------------------- 1 | import { takeEvery, delay } from 'redux-saga' 2 | import { call, put, fork, select } from 'redux-saga/effects' 3 | import * as ActionTypes from 'actions/ActionTypes'; 4 | import * as messageApi from 'services/message'; 5 | import { fetchMessage, toggleLoading } from 'actions/message'; 6 | 7 | // FETCH INITIAL / RECENT / OLD MESSAGES 8 | function* fetch(action) { 9 | 10 | // Create a Request 11 | const { response, error } = yield call(messageApi.fetch, action.payload); 12 | 13 | 14 | if(response) { 15 | // SUCCEED 16 | yield put({type: ActionTypes.FETCH_MESSAGE.SUCCESS, payload: { response, ...action.payload }}); 17 | } else { 18 | // ERROR HAS OCCURRED 19 | yield put({type: ActionTypes.FETCH_MESSAGE.FAILURE, payload: { error }}); 20 | if(error.code !== 'ECONNABORTED') { 21 | // IF IT IS NOT TIMED OUT, WAIT FOR 10 SEC, SO THAT IT DOES NOT DOS THE SERVER :) 22 | yield delay(1000*10); 23 | } 24 | } 25 | 26 | 27 | // IF USER JUST DID THE INITIAL OR RECENT MESSAGE LOADING 28 | if(action.payload.initial || action.payload.latest) { 29 | yield delay(1); // SHOULD WAIT 1ms TO REPEAT THE TASK WIHTOUT ISSUES 30 | const getMessageData = (state) => state.message.data; // ACCESS THE STORE USING SELECT 31 | const data = yield select(getMessageData); 32 | const tail = (data.length > 0) ? data[data.length-1]._id : ''; // GET THE LAST MEMO ID 33 | yield put(fetchMessage({initial: false, latest: true, pivot: tail})); // DISPATCH ANOTHER FETCHING ACTION 34 | } 35 | 36 | } 37 | 38 | // WRITES A NEW MESSAAGE 39 | function* write(action) { 40 | 41 | // CREATE A REQUEST 42 | const { response, error } = yield call(messageApi.write, action.payload); 43 | 44 | if(response) { 45 | yield put({type: ActionTypes.WRITE_MESSAGE.SUCCESS, payload: { response }}); 46 | } else { 47 | yield put({type: ActionTypes.WRITE_MESSAGE.ERROR, payload: { error }}); 48 | } 49 | } 50 | 51 | 52 | // ACTION WATCHERS 53 | 54 | function* watchFetch() { 55 | yield* takeEvery(ActionTypes.FETCH_MESSAGE.REQUEST, fetch); 56 | } 57 | 58 | function* watchWrite() { 59 | yield* takeEvery(ActionTypes.WRITE_MESSAGE.REQUEST, write); 60 | } 61 | 62 | export default function* messageSaga() { 63 | yield fork(watchFetch); 64 | yield fork(watchWrite); 65 | } 66 | -------------------------------------------------------------------------------- /client/src/sagas/session.js: -------------------------------------------------------------------------------- 1 | import { takeEvery } from 'redux-saga' 2 | import { call, put, fork } from 'redux-saga/effects' 3 | import * as ActionTypes from 'actions/ActionTypes'; 4 | import * as sessionApi from 'services/session'; 5 | 6 | 7 | // GETS THE SESSION AND PUT IN THE SESSION STORE 8 | 9 | function* get(action) { 10 | 11 | const { response, error } = yield call(sessionApi.get); 12 | 13 | if(response) { 14 | yield put({type: ActionTypes.GET_SESSION.SUCCESS, payload: { response }}); 15 | } else { 16 | yield put({type: ActionTypes.GET_SESSION.ERROR, payload: { error }}); 17 | } 18 | } 19 | 20 | function* watchGet() { 21 | yield* takeEvery(ActionTypes.GET_SESSION.REQUEST, get); 22 | } 23 | 24 | 25 | export default function* sessionSaga() { 26 | yield fork(watchGet); 27 | } 28 | -------------------------------------------------------------------------------- /client/src/services/message.js: -------------------------------------------------------------------------------- 1 | import request from 'utils/request'; 2 | 3 | export function fetch({initial, latest=false, pivot}) { 4 | // load inital data 5 | if(initial) { 6 | return request('/api/message'); 7 | } 8 | 9 | 10 | if(latest) { 11 | // load recent data 12 | return request(`/api/message/recent/${pivot}`, 'get', {}, { timeout: 30 * 1000 }); 13 | } else { 14 | // load old data 15 | return request(`/api/message/old/${pivot}`); 16 | } 17 | } 18 | 19 | export function write({message, uid}) { 20 | // write a message 21 | return request('/api/message', 'post', { message, uid }); 22 | } 23 | -------------------------------------------------------------------------------- /client/src/services/session.js: -------------------------------------------------------------------------------- 1 | import request from 'utils/request'; 2 | 3 | export function get() { 4 | return request('/api/session'); 5 | } 6 | -------------------------------------------------------------------------------- /client/src/stylesheets/base/_all.scss: -------------------------------------------------------------------------------- 1 | @import 'variables'; 2 | @import 'media'; 3 | @import 'typography'; 4 | @import 'reset'; 5 | -------------------------------------------------------------------------------- /client/src/stylesheets/base/_media.scss: -------------------------------------------------------------------------------- 1 | @mixin phone { 2 | @media (max-width: #{$tablet-width - 1px}) { 3 | @content; 4 | } 5 | } 6 | 7 | @mixin tablet { 8 | @media (min-width: #{$tablet-width}) and (max-width: #{$desktop-width - 1px}) { 9 | @content; 10 | } 11 | } 12 | 13 | @mixin phone-and-tablet { 14 | @media (max-width: #{$desktop-width - 1px}) { 15 | @content; 16 | } 17 | } 18 | 19 | @mixin tablet-and-desktop { 20 | @media (min-width: #{$tablet-width}) { 21 | @content; 22 | } 23 | } 24 | 25 | @mixin desktop { 26 | @media (min-width: #{$desktop-width}) { 27 | @content; 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /client/src/stylesheets/base/_reset.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'NanumGothic', 'Segoe UI', 'Malgun Gothic', 'Roboto'; 3 | margin: 0px; 4 | } 5 | -------------------------------------------------------------------------------- /client/src/stylesheets/base/_typography.scss: -------------------------------------------------------------------------------- 1 | @import url(https://fonts.googleapis.com/css?family=Cinzel); 2 | -------------------------------------------------------------------------------- /client/src/stylesheets/base/_variables.scss: -------------------------------------------------------------------------------- 1 | $phone-width: 640px; 2 | $tablet-width: 768px; 3 | $desktop-width: 1024px; 4 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_Input.scss: -------------------------------------------------------------------------------- 1 | .input-container { 2 | background-color: rgba(255,255, 255, 0.1); 3 | border: 1px solid rgba(0,0, 0, 0.6); 4 | border-width: 1px 0px 1px 0px; 5 | position: absolute; 6 | width: 100%; 7 | bottom: 10px; 8 | } 9 | 10 | .input-container input { 11 | height: 43px; 12 | font-size: 15px; 13 | width: 100%; 14 | box-sizing: border-box; 15 | padding: 10px; 16 | background: transparent; 17 | border: none; 18 | background-color: none; 19 | color: white 20 | } 21 | 22 | .input-container input:focus { 23 | outline: none; 24 | } 25 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_Message.scss: -------------------------------------------------------------------------------- 1 | .message { 2 | word-wrap: break-word; 3 | padding: 10px; 4 | } 5 | 6 | .message + .message { 7 | border-top: 1px solid rgba(0,0, 0, 0.9); 8 | } 9 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_MessageList.scss: -------------------------------------------------------------------------------- 1 | .message-list { 2 | box-shadow: inset 0 10px 10px -10px rgba(0,0,0,0.90), 3 | inset 0 -10px 10px -10px rgba(0,0,0,0.90); 4 | position: absolute; 5 | width: 100%; 6 | top: 90px; 7 | bottom: 54px; 8 | border-top: 1px solid 1px solid rgba(0,0, 0, 0.6); 9 | } 10 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_Sidebar.scss: -------------------------------------------------------------------------------- 1 | .sidebar { 2 | background-color: rgba(29, 31, 32, 0.5); 3 | color: white; 4 | position: absolute; 5 | right: 0px; 6 | width: 400px; 7 | height: 100%; 8 | @include tablet-and-desktop { 9 | border-left: 1px solid black; 10 | box-shadow: inset 13px 0px 10px -10px rgba(0,0,0,0.70); 11 | } 12 | @include phone { 13 | width: 100% 14 | } 15 | } 16 | 17 | .logo { 18 | text-align: center; 19 | margin:30px 0 30px 0; 20 | font-size: 40px; 21 | font-family: 'Cinzel'; 22 | } 23 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_Spinner.scss: -------------------------------------------------------------------------------- 1 | .sk-circle { 2 | margin-top: 10px; 3 | margin-bottom: 10px; 4 | margin-left: auto; 5 | margin-right: auto; 6 | width: 40px; 7 | height: 40px; 8 | position: relative; 9 | } 10 | .sk-circle .sk-child { 11 | width: 100%; 12 | height: 100%; 13 | position: absolute; 14 | left: 0; 15 | top: 0; 16 | } 17 | .sk-circle .sk-child:before { 18 | content: ''; 19 | display: block; 20 | margin: 0 auto; 21 | width: 15%; 22 | height: 15%; 23 | background-color: white; 24 | border-radius: 100%; 25 | -webkit-animation: sk-circleBounceDelay 0.5s infinite ease-in-out both; 26 | animation: sk-circleBounceDelay 1.2s infinite ease-in-out both; 27 | } 28 | .sk-circle .sk-circle2 { 29 | -webkit-transform: rotate(30deg); 30 | -ms-transform: rotate(30deg); 31 | transform: rotate(30deg); } 32 | .sk-circle .sk-circle3 { 33 | -webkit-transform: rotate(60deg); 34 | -ms-transform: rotate(60deg); 35 | transform: rotate(60deg); } 36 | .sk-circle .sk-circle4 { 37 | -webkit-transform: rotate(90deg); 38 | -ms-transform: rotate(90deg); 39 | transform: rotate(90deg); } 40 | .sk-circle .sk-circle5 { 41 | -webkit-transform: rotate(120deg); 42 | -ms-transform: rotate(120deg); 43 | transform: rotate(120deg); } 44 | .sk-circle .sk-circle6 { 45 | -webkit-transform: rotate(150deg); 46 | -ms-transform: rotate(150deg); 47 | transform: rotate(150deg); } 48 | .sk-circle .sk-circle7 { 49 | -webkit-transform: rotate(180deg); 50 | -ms-transform: rotate(180deg); 51 | transform: rotate(180deg); } 52 | .sk-circle .sk-circle8 { 53 | -webkit-transform: rotate(210deg); 54 | -ms-transform: rotate(210deg); 55 | transform: rotate(210deg); } 56 | .sk-circle .sk-circle9 { 57 | -webkit-transform: rotate(240deg); 58 | -ms-transform: rotate(240deg); 59 | transform: rotate(240deg); } 60 | .sk-circle .sk-circle10 { 61 | -webkit-transform: rotate(270deg); 62 | -ms-transform: rotate(270deg); 63 | transform: rotate(270deg); } 64 | .sk-circle .sk-circle11 { 65 | -webkit-transform: rotate(300deg); 66 | -ms-transform: rotate(300deg); 67 | transform: rotate(300deg); } 68 | .sk-circle .sk-circle12 { 69 | -webkit-transform: rotate(330deg); 70 | -ms-transform: rotate(330deg); 71 | transform: rotate(330deg); } 72 | .sk-circle .sk-circle2:before { 73 | -webkit-animation-delay: -1.1s; 74 | animation-delay: -1.1s; } 75 | .sk-circle .sk-circle3:before { 76 | -webkit-animation-delay: -1s; 77 | animation-delay: -1s; } 78 | .sk-circle .sk-circle4:before { 79 | -webkit-animation-delay: -0.9s; 80 | animation-delay: -0.9s; } 81 | .sk-circle .sk-circle5:before { 82 | -webkit-animation-delay: -0.8s; 83 | animation-delay: -0.8s; } 84 | .sk-circle .sk-circle6:before { 85 | -webkit-animation-delay: -0.7s; 86 | animation-delay: -0.7s; } 87 | .sk-circle .sk-circle7:before { 88 | -webkit-animation-delay: -0.6s; 89 | animation-delay: -0.6s; } 90 | .sk-circle .sk-circle8:before { 91 | -webkit-animation-delay: -0.5s; 92 | animation-delay: -0.5s; } 93 | .sk-circle .sk-circle9:before { 94 | -webkit-animation-delay: -0.4s; 95 | animation-delay: -0.4s; } 96 | .sk-circle .sk-circle10:before { 97 | -webkit-animation-delay: -0.3s; 98 | animation-delay: -0.3s; } 99 | .sk-circle .sk-circle11:before { 100 | -webkit-animation-delay: -0.2s; 101 | animation-delay: -0.2s; } 102 | .sk-circle .sk-circle12:before { 103 | -webkit-animation-delay: -0.1s; 104 | animation-delay: -0.1s; } 105 | 106 | @-webkit-keyframes sk-circleBounceDelay { 107 | 0%, 80%, 100% { 108 | -webkit-transform: scale(0); 109 | transform: scale(0); 110 | } 40% { 111 | -webkit-transform: scale(1); 112 | transform: scale(1); 113 | } 114 | } 115 | 116 | @keyframes sk-circleBounceDelay { 117 | 0%, 80%, 100% { 118 | -webkit-transform: scale(0); 119 | transform: scale(0); 120 | } 40% { 121 | -webkit-transform: scale(1); 122 | transform: scale(1); 123 | } 124 | } 125 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_ToggleVideo.scss: -------------------------------------------------------------------------------- 1 | .toggle-video { 2 | background-color: rgba(255, 255, 255, 0.1); 3 | color: white; 4 | top: 20px; 5 | left: 20px; 6 | position: absolute; 7 | padding: 8px; 8 | border: 1px solid white; 9 | border-radius: 2px; 10 | cursor: pointer; 11 | 12 | -moz-user-select: -moz-none; 13 | -khtml-user-select: none; 14 | -webkit-user-select: none; 15 | 16 | /* 17 | Introduced in IE 10. 18 | See http://ie.microsoft.com/testdrive/HTML5/msUserSelect/ 19 | */ 20 | -ms-user-select: none; 21 | user-select: none; 22 | 23 | @include phone-and-tablet { 24 | display: none; 25 | } 26 | 27 | } 28 | 29 | .toggle-video:hover { 30 | background-color: rgba(255, 255, 255, 0.2); 31 | } 32 | 33 | .toggle-video:active { 34 | background-color: rgba(255, 255, 255, 0.3); 35 | } 36 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_VideoScreen.scss: -------------------------------------------------------------------------------- 1 | .video-background { 2 | background: gray; 3 | position: fixed; 4 | top: 0; 5 | right: 0; 6 | bottom: 0; 7 | left: 0; 8 | z-index: -99; 9 | background: url(https://wallpaperscraft.com/image/road_night_lights_88183_225x300.jpg) no-repeat center center fixed; 10 | -webkit-background-size: cover; 11 | -moz-background-size: cover; 12 | -o-background-size: cover; 13 | background-size: cover; 14 | 15 | } 16 | 17 | .video-foreground, 18 | .video-background iframe { 19 | position: absolute; 20 | top: 50%; 21 | left: 50%; 22 | width: auto; 23 | height: auto; 24 | -webkit-transform: translate(-50%, -50%); 25 | -moz-transform: translate(-50%, -50%); 26 | -ms-transform: translate(-50%, -50%); 27 | transform: translate(-50%, -50%); 28 | pointer-events: none; 29 | min-width: 100%; 30 | min-height: 100%; 31 | @include phone-and-tablet { 32 | display: none; 33 | } 34 | } 35 | 36 | 37 | @media (min-aspect-ratio: 16 / 9) { 38 | .video-foreground { 39 | height: 300%; 40 | min-height: 600px; 41 | 42 | } 43 | } 44 | 45 | 46 | @media (max-aspect-ratio: 16 / 9) { 47 | .video-foreground { 48 | width: 300%; 49 | min-width: 600px; 50 | 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /client/src/stylesheets/components/_all.scss: -------------------------------------------------------------------------------- 1 | @import 'VideoScreen'; 2 | @import 'Sidebar'; 3 | @import 'Input'; 4 | @import 'MessageList'; 5 | @import 'Message'; 6 | @import 'Spinner'; 7 | @import 'ToggleVideo'; 8 | -------------------------------------------------------------------------------- /client/src/stylesheets/main.scss: -------------------------------------------------------------------------------- 1 | @import 'base/all'; 2 | @import 'components/all'; 3 | -------------------------------------------------------------------------------- /client/src/utils/action.js: -------------------------------------------------------------------------------- 1 | /* 2 | Creates $NAME_REQUEST, $NAME_SUCESS, $NAME_FAILURE actions 3 | accessed by $NAME.$STATE, e.g. FETCH_MESSAGE.REQUEST 4 | */ 5 | 6 | const REQUEST = 'REQUEST' 7 | const SUCCESS = 'SUCCESS' 8 | const FAILURE = 'FAILURE' 9 | 10 | export default function createRequestTypes(base) { 11 | return [REQUEST, SUCCESS, FAILURE].reduce((acc, type) => { 12 | acc[type] = `${base}_${type}` 13 | return acc 14 | }, {}) 15 | } 16 | -------------------------------------------------------------------------------- /client/src/utils/request.js: -------------------------------------------------------------------------------- 1 | import axios from 'axios'; 2 | 3 | /* 4 | Returns a Axios Request Promise 5 | */ 6 | 7 | export default function request(url, method = 'get', data, config) { 8 | return axios({ 9 | method, 10 | url, 11 | data, 12 | ...config 13 | }).then( 14 | response => { 15 | return {response}; 16 | } 17 | ).catch( 18 | error => { 19 | return {error}; 20 | } 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /server/.env.cpy: -------------------------------------------------------------------------------- 1 | PORT=3000 2 | DB_URI="mongodb://host:port/saysomething" 3 | -------------------------------------------------------------------------------- /server/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .env 3 | -------------------------------------------------------------------------------- /server/build/controllers/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | exports.session = exports.message = undefined; 7 | 8 | var _message = require('./message'); 9 | 10 | var _message2 = _interopRequireDefault(_message); 11 | 12 | var _session = require('./session'); 13 | 14 | var _session2 = _interopRequireDefault(_session); 15 | 16 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 17 | 18 | exports.message = _message2.default; 19 | exports.session = _session2.default; -------------------------------------------------------------------------------- /server/build/controllers/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _message = require('../models/message'); 8 | 9 | var _message2 = _interopRequireDefault(_message); 10 | 11 | var _mongoose = require('mongoose'); 12 | 13 | var _mongoose2 = _interopRequireDefault(_mongoose); 14 | 15 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 16 | 17 | exports.default = { 18 | /* 19 | API: POST /api/write 20 | body: {message: 'message'} 21 | description: Write a new message 22 | */ 23 | write: function write(req, res) { 24 | var session = req.session; 25 | 26 | // check color existancy 27 | if (!session.color) { 28 | return res.status(403).json({ 29 | error: { 30 | code: 1, 31 | message: 'Invalid Request' 32 | } 33 | }); 34 | } 35 | 36 | // check message validity 37 | if (typeof req.body.message !== 'string' || typeof req.body.uid !== 'string') { 38 | return res.status(400).json({ 39 | error: { 40 | code: 2, 41 | message: 'Invalid Message' 42 | } 43 | }); 44 | } 45 | 46 | if (req.body.message === '') { 47 | return res.status(400).json({ 48 | error: { 49 | code: 3, 50 | message: 'Message is empty' 51 | } 52 | }); 53 | } 54 | 55 | if (req.body.message.length > 50) { 56 | return res.status(400).json({ 57 | error: { 58 | code: 5, 59 | message: 'Message is too long' 60 | } 61 | }); 62 | } 63 | 64 | var msg = new _message2.default({ 65 | message: req.body.message, 66 | uid: req.body.uid, 67 | color: session.color 68 | }); 69 | 70 | msg.save().then(function () { 71 | var cache = res.app.get('cache'); 72 | cache.notifyWorker(); 73 | return res.json({ 74 | success: true 75 | }); 76 | }).catch(function (error) { 77 | throw error; 78 | }); 79 | }, 80 | list: { 81 | /* 82 | API: GET /api/message 83 | description: Loads initial message data 84 | */ 85 | initial: function initial(req, res) { 86 | var cache = req.app.get('cache'); 87 | return res.json(cache.messages); 88 | }, 89 | 90 | /* 91 | API: GET /api/message/recent 92 | description: instantly loads initial data 93 | */ 94 | 95 | initRecent: function initRecent(req, res) { 96 | var cache = req.app.get('cache'); 97 | 98 | if (cache.messages.length > 0) { 99 | // this is an invalid request 100 | return res.status(400).json({ 101 | error: { 102 | message: 'Invalid ID', 103 | code: 1 104 | } 105 | }); 106 | } 107 | 108 | // it is empty, so wait for the new 109 | var waitForNewMemo = function waitForNewMemo() { 110 | return new Promise(function (resolve, reject) { 111 | var timeoutId = void 0; 112 | var check = function check() { 113 | if (cache.messages.length > 0) { 114 | // if the head is different 115 | resolve(); // resolve the Promise 116 | return; 117 | } 118 | timeoutId = setTimeout(check, 5); // or else, repeat this 119 | }; 120 | check(); 121 | setTimeout(function () { 122 | clearTimeout(timeoutId); 123 | reject(); 124 | }, 1000 * 30); // timeout after 30 seconds 125 | }); 126 | }; 127 | 128 | waitForNewMemo().then(function () { 129 | var recentMsg = cache.messages; 130 | return res.json(recentMsg); 131 | }).catch(function () { 132 | return res.json([]); // timeout, return empty array. 133 | }); 134 | }, 135 | /* 136 | API: GET /api/list/old/:id 137 | params: 138 | id: message id 139 | description: Load messages older than given the given id 140 | */ 141 | old: function old(req, res) { 142 | var id = req.params.id; 143 | 144 | // check id validity 145 | if (!_mongoose2.default.Types.ObjectId.isValid(id)) { 146 | return res.status(400).json({ 147 | error: { 148 | message: 'Invalid ID', 149 | code: 1 150 | } 151 | }); 152 | } 153 | 154 | _message2.default.find({ _id: { $lt: id } }).sort({ _id: -1 }).limit(25).exec().then(function (messages) { 155 | return res.json(messages.reverse()); 156 | }).catch(function (error) { 157 | throw error; 158 | }); 159 | }, 160 | /* 161 | API: GET /api/list/recent/:id 162 | params: 163 | id: message id 164 | description: Load messages newer than given the given id 165 | */ 166 | recent: function recent(req, res) { 167 | 168 | var id = req.params.id; 169 | var cache = req.app.get('cache'); 170 | 171 | // check id validity 172 | if (!_mongoose2.default.Types.ObjectId.isValid(id)) { 173 | return res.status(400).json({ 174 | error: { 175 | message: 'Invalid ID', 176 | code: 1 177 | } 178 | }); 179 | } 180 | 181 | var recentMsg = cache.getRecentMsg(id); 182 | 183 | if (typeof recentMsg === 'undefined') { 184 | return res.status(400).json({ 185 | error: { 186 | message: 'Invalid ID', 187 | code: 1 188 | } 189 | }); 190 | } 191 | 192 | if (recentMsg.length > 0) { 193 | return res.json(recentMsg); 194 | } 195 | 196 | /* if recentMsg is undefined (which means given id does not exist in 197 | the cache), load directly from the mongod database. 198 | if(typeof recentMsg === 'undefined') { 199 | Message.find({_id: { $gt: id}}) 200 | .sort({_id: -1}) 201 | .limit(20) 202 | .exec() 203 | .then( 204 | (messages) => { 205 | return res.json(messages.reverse()); 206 | } 207 | ).catch( 208 | (error) => { 209 | throw error; 210 | } 211 | ); 212 | } else { 213 | // if there is more than one message, respond to the client 214 | if(recentMsg.length > 0) { 215 | return res.json(recentMsg); 216 | } 217 | }*/ 218 | 219 | /* if the tail matches id, it means there is no new memo. In this case, 220 | wait until there is a new memo. When 30 seconds pass, just return 221 | an empty array */ 222 | 223 | if (cache.tail === id) { 224 | var waitForNewMemo = function waitForNewMemo() { 225 | return new Promise(function (resolve, reject) { 226 | var timeoutId = void 0; 227 | var check = function check() { 228 | if (id !== cache.tail) { 229 | // if the head is different 230 | resolve(); // resolve the Promise 231 | return; 232 | } 233 | timeoutId = setTimeout(check, 5); // or else, repeat this 234 | }; 235 | check(); 236 | setTimeout(function () { 237 | clearTimeout(timeoutId); 238 | reject(); 239 | }, 1000 * 30); // timeout after 30 seconds 240 | }); 241 | }; 242 | 243 | waitForNewMemo().then(function () { 244 | recentMsg = cache.getRecentMsg(id); 245 | return res.json(recentMsg); 246 | }).catch(function () { 247 | return res.json([]); // timeout, return empty array. 248 | }); 249 | } 250 | } 251 | } 252 | }; -------------------------------------------------------------------------------- /server/build/controllers/session.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | /** 7 | * Creates Random Color 8 | * @return {Array} RGB value in a array format: [r,g,b] 9 | */ 10 | function generateColor() { 11 | var r = Math.floor(Math.random() * 255); 12 | var g = Math.floor(Math.random() * 255); 13 | var b = Math.floor(Math.random() * 255); 14 | return [r, g, b]; 15 | } 16 | 17 | /* 18 | API: GET / 19 | description: Returns or creates a session, which is in a random color format 20 | */ 21 | var session = function session(req, res) { 22 | var session = req.session; 23 | // if color is not set, set a new color 24 | if (!session.color) { 25 | session.color = generateColor(); 26 | } 27 | 28 | // return the color 29 | return res.json({ 30 | color: session.color 31 | }); 32 | }; 33 | 34 | exports.default = session; -------------------------------------------------------------------------------- /server/build/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var _express = require('express'); 4 | 5 | var _express2 = _interopRequireDefault(_express); 6 | 7 | var _bodyParser = require('body-parser'); 8 | 9 | var _bodyParser2 = _interopRequireDefault(_bodyParser); 10 | 11 | var _morgan = require('morgan'); 12 | 13 | var _morgan2 = _interopRequireDefault(_morgan); 14 | 15 | var _expressSession = require('express-session'); 16 | 17 | var _expressSession2 = _interopRequireDefault(_expressSession); 18 | 19 | var _compression = require('compression'); 20 | 21 | var _compression2 = _interopRequireDefault(_compression); 22 | 23 | var _mongoose = require('mongoose'); 24 | 25 | var _mongoose2 = _interopRequireDefault(_mongoose); 26 | 27 | var _connectMongo = require('connect-mongo'); 28 | 29 | var _connectMongo2 = _interopRequireDefault(_connectMongo); 30 | 31 | var _routes = require('./routes'); 32 | 33 | var _routes2 = _interopRequireDefault(_routes); 34 | 35 | var _cors = require('cors'); 36 | 37 | var _cors2 = _interopRequireDefault(_cors); 38 | 39 | var _MessageCache = require('./utils/MessageCache'); 40 | 41 | var _MessageCache2 = _interopRequireDefault(_MessageCache); 42 | 43 | var _dotenv = require('dotenv'); 44 | 45 | var _dotenv2 = _interopRequireDefault(_dotenv); 46 | 47 | var _path = require('path'); 48 | 49 | var _path2 = _interopRequireDefault(_path); 50 | 51 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 52 | 53 | // LOAD ENV CONFIG 54 | _dotenv2.default.config(); 55 | 56 | var app = (0, _express2.default)(); 57 | var port = process.env.PORT || 3000; 58 | 59 | var MongoStore = (0, _connectMongo2.default)(_expressSession2.default); 60 | 61 | // SETUP MIDDLEWARE 62 | app.use(_bodyParser2.default.json()); 63 | app.use((0, _morgan2.default)('tiny')); 64 | app.use((0, _expressSession2.default)({ 65 | secret: '@#@$MYSIGN#@$#$', 66 | resave: false, 67 | saveUninitialized: true, 68 | cookie: { 69 | maxAge: 14 * 24 * 60 * 60 * 1000 // 14 DAYS 70 | }, 71 | store: new MongoStore({ 72 | mongooseConnection: _mongoose2.default.connection, 73 | ttl: 14 * 24 * 60 * 60 74 | }) 75 | })); 76 | app.use((0, _cors2.default)()); 77 | app.use((0, _compression2.default)()); 78 | 79 | // SERVE STATIC FILES 80 | app.use('/', _express2.default.static(_path2.default.join(__dirname, './../../client/public'))); 81 | 82 | // SETUP ROUTER 83 | app.use('/api', _routes2.default); 84 | 85 | /* handle error */ 86 | app.use(function (err, req, res, next) { 87 | console.error(err.stack); 88 | res.status(500).json({ 89 | error: { 90 | message: 'Something Broke!', 91 | code: 0 92 | } 93 | }); 94 | next(); 95 | }); 96 | 97 | var cache = new _MessageCache2.default(); 98 | app.set('cache', cache); 99 | 100 | _mongoose2.default.Promise = global.Promise; 101 | var db = _mongoose2.default.connection; 102 | db.on('error', console.error); 103 | db.once('open', function () { 104 | console.log('Connected to mongod server'); 105 | cache.startWorker(); 106 | }); 107 | 108 | _mongoose2.default.connect(process.env.DB_URI); 109 | 110 | app.listen(port, function () { 111 | console.log('Express is running on port ' + port); 112 | }); -------------------------------------------------------------------------------- /server/build/models/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _mongoose = require('mongoose'); 8 | 9 | var _mongoose2 = _interopRequireDefault(_mongoose); 10 | 11 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 12 | 13 | var Message = new _mongoose.Schema({ 14 | message: String, 15 | date: { type: Date, default: Date.now }, 16 | uid: String, 17 | color: [] // stores color in r, g, b array 18 | }); 19 | 20 | exports.default = _mongoose2.default.model('message', Message); -------------------------------------------------------------------------------- /server/build/routes/index.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _express = require('express'); 8 | 9 | var _express2 = _interopRequireDefault(_express); 10 | 11 | var _message = require('./message'); 12 | 13 | var _message2 = _interopRequireDefault(_message); 14 | 15 | var _session = require('./session'); 16 | 17 | var _session2 = _interopRequireDefault(_session); 18 | 19 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 20 | 21 | var router = _express2.default.Router(); 22 | 23 | router.use('/*', function (req, res, next) { 24 | res.set("Connection", "keep-alive"); 25 | next(); 26 | }); 27 | router.use('/message', _message2.default); 28 | router.use('/session', _session2.default); 29 | 30 | exports.default = router; -------------------------------------------------------------------------------- /server/build/routes/message.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _express = require('express'); 8 | 9 | var _express2 = _interopRequireDefault(_express); 10 | 11 | var _controllers = require('../controllers'); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | var router = _express2.default.Router(); 16 | 17 | router.get('/', _controllers.message.list.initial); 18 | router.get('/old/:id', _controllers.message.list.old); 19 | router.get('/recent', _controllers.message.list.initRecent); 20 | router.get('/recent/:id', _controllers.message.list.recent); 21 | router.post('/', _controllers.message.write); 22 | 23 | exports.default = router; -------------------------------------------------------------------------------- /server/build/routes/session.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _express = require('express'); 8 | 9 | var _express2 = _interopRequireDefault(_express); 10 | 11 | var _controllers = require('../controllers'); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | var router = _express2.default.Router(); 16 | 17 | router.get('/', _controllers.session); 18 | 19 | exports.default = router; -------------------------------------------------------------------------------- /server/build/utils/MessageCache.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | Object.defineProperty(exports, "__esModule", { 4 | value: true 5 | }); 6 | 7 | var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }(); 8 | 9 | var _message = require('../models/message'); 10 | 11 | var _message2 = _interopRequireDefault(_message); 12 | 13 | function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } 14 | 15 | function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } } 16 | 17 | var MessageCache = function () { 18 | function MessageCache() { 19 | _classCallCheck(this, MessageCache); 20 | 21 | this.messages = []; 22 | this.idMap = []; 23 | this.tail = ''; 24 | this.pending = true; // to do initial loading 25 | this.timeoutId = undefined; 26 | } 27 | 28 | _createClass(MessageCache, [{ 29 | key: 'notifyWorker', 30 | value: function notifyWorker() { 31 | this.pending = true; 32 | } 33 | }, { 34 | key: 'startWorker', 35 | value: function startWorker() { 36 | var _this = this; 37 | 38 | var loadData = function loadData() { 39 | return _message2.default.find().sort({ _id: -1 }).limit(25).exec().then(function (messages) { 40 | if (messages.length == 0) { 41 | return false; 42 | } 43 | 44 | messages.reverse(); // reverse the array, since queried result is in reversed order 45 | _this.messages = messages; 46 | _this.idMap = messages.map(function (msg) { 47 | return msg._id.toString(); 48 | }); 49 | _this.tail = _this.idMap[messages.length - 1]; 50 | return true; 51 | }).catch(function (error) { 52 | console.error(error.stack); 53 | return false; 54 | }); 55 | }; 56 | 57 | var work = function work() { 58 | if (_this.pending) { 59 | console.log("Data Loading.."); 60 | _this.pending = false; 61 | loadData().then(function (success) { 62 | if (!success) { 63 | console.error('Data loading has failed, retry in 3 seconds'); 64 | _this.pending = true; 65 | _this.timeoutId = setTimeout(work, 3000); 66 | return; 67 | } 68 | console.log('Cache is updated - ', _this.messages[0]._id); 69 | _this.timeoutId = setTimeout(work, 5); 70 | }); 71 | 72 | return; 73 | } 74 | 75 | _this.timeoutId = setTimeout(work, 5); 76 | }; 77 | 78 | work(); 79 | } 80 | }, { 81 | key: 'getRecentMsg', 82 | value: function getRecentMsg(id) { 83 | 84 | var index = this.idMap.indexOf(id); 85 | 86 | // do not use cache when id does not exist 87 | if (this.idMap.indexOf(id) == -1) return undefined; 88 | 89 | return this.messages.slice(index + 1, this.messages.length); 90 | } 91 | }]); 92 | 93 | return MessageCache; 94 | }(); 95 | 96 | exports.default = MessageCache; -------------------------------------------------------------------------------- /server/config/eslint.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | "env": { 3 | "es6": true, 4 | "node": true 5 | }, 6 | "extends": "eslint:recommended", 7 | "parserOptions": { 8 | "sourceType": "module" 9 | }, 10 | "rules": { 11 | "indent": [ 12 | "error", 13 | 4 14 | ], 15 | "semi": [ 16 | "error", 17 | "always" 18 | ], 19 | "no-console": ["error", { allow: ["warn", "error", "log"] }] 20 | }, 21 | 22 | }; 23 | -------------------------------------------------------------------------------- /server/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "saysomething-back-end", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "index.js", 6 | "scripts": { 7 | "build": "babel src -d build --presets=es2015,stage-0", 8 | "start": "node build", 9 | "dev": "node scripts/development.js", 10 | "test": "echo \"Error: no test specified\" && exit 1" 11 | }, 12 | "author": "", 13 | "license": "ISC", 14 | "dependencies": { 15 | "body-parser": "^1.15.2", 16 | "compression": "^1.6.2", 17 | "connect-mongo": "^1.3.2", 18 | "cors": "^2.7.1", 19 | "dotenv": "^2.0.0", 20 | "express": "^4.14.0", 21 | "express-session": "^1.14.0", 22 | "mongoose": "^4.5.8", 23 | "morgan": "^1.7.0" 24 | }, 25 | "devDependencies": { 26 | "babel-core": "^6.13.2", 27 | "babel-preset-es2015": "^6.13.2", 28 | "babel-preset-stage-0": "^6.5.0", 29 | "nodemon": "^1.10.0" 30 | }, 31 | "eslintConfig": { 32 | "extends": "./config/eslint.js" 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /server/scripts/development.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | process.env.NODE_ENV = 'development'; 4 | 5 | var nodemon = require('nodemon'); 6 | nodemon('--exec babel-node --presets=es2015 ./src --watch ./src'); 7 | 8 | nodemon.on('start', function () { 9 | console.log('[nodemon] App has started'); 10 | }).on('quit', function () { 11 | console.log('[nodemon] App has quit'); 12 | }).on('restart', function (files) { 13 | console.log('[nodemon] App restarted due to:', files); 14 | }); -------------------------------------------------------------------------------- /server/src/controllers/index.js: -------------------------------------------------------------------------------- 1 | import message from './message'; 2 | import session from './session'; 3 | 4 | export { 5 | message, 6 | session 7 | }; 8 | -------------------------------------------------------------------------------- /server/src/controllers/message.js: -------------------------------------------------------------------------------- 1 | import Message from '../models/message'; 2 | import mongoose from 'mongoose'; 3 | 4 | export default { 5 | /* 6 | API: POST /api/write 7 | body: {message: 'message'} 8 | description: Write a new message 9 | */ 10 | write: (req, res) => { 11 | const session = req.session; 12 | 13 | // check color existancy 14 | if(!session.color) { 15 | return res.status(403).json({ 16 | error: { 17 | code: 1, 18 | message: 'Invalid Request' 19 | } 20 | }); 21 | } 22 | 23 | // check message validity 24 | if(typeof req.body.message !== 'string' || typeof req.body.uid !== 'string') { 25 | return res.status(400).json({ 26 | error: { 27 | code: 2, 28 | message: 'Invalid Message' 29 | } 30 | }); 31 | } 32 | 33 | if(req.body.message === '') { 34 | return res.status(400).json({ 35 | error: { 36 | code: 3, 37 | message: 'Message is empty' 38 | } 39 | }); 40 | } 41 | 42 | if(req.body.message.length > 50) { 43 | return res.status(400).json({ 44 | error: { 45 | code: 5, 46 | message: 'Message is too long' 47 | } 48 | }); 49 | } 50 | 51 | const msg = new Message({ 52 | message: req.body.message, 53 | uid: req.body.uid, 54 | color: session.color 55 | }); 56 | 57 | msg.save().then( 58 | () => { 59 | const cache = res.app.get('cache'); 60 | cache.notifyWorker(); 61 | return res.json({ 62 | success: true 63 | }); 64 | } 65 | ).catch( 66 | (error) => { 67 | throw error; 68 | } 69 | ); 70 | }, 71 | list: { 72 | /* 73 | API: GET /api/message 74 | description: Loads initial message data 75 | */ 76 | initial: (req, res) => { 77 | const cache = req.app.get('cache'); 78 | return res.json( 79 | cache.messages 80 | ); 81 | 82 | }, 83 | 84 | /* 85 | API: GET /api/message/recent 86 | description: instantly loads initial data 87 | */ 88 | 89 | initRecent: (req, res) => { 90 | const cache = req.app.get('cache'); 91 | 92 | 93 | if(cache.messages.length > 0) { 94 | // this is an invalid request 95 | return res.status(400).json({ 96 | error: { 97 | message: 'Invalid ID', 98 | code: 1 99 | } 100 | }); 101 | } 102 | 103 | // it is empty, so wait for the new 104 | const waitForNewMemo = () => { 105 | return new Promise( 106 | (resolve, reject) => { 107 | let timeoutId; 108 | const check = () => { 109 | if(cache.messages.length > 0) { 110 | // if the head is different 111 | resolve(); // resolve the Promise 112 | return; 113 | } 114 | timeoutId = setTimeout(check, 5); // or else, repeat this 115 | }; 116 | check(); 117 | setTimeout( 118 | () => { 119 | clearTimeout(timeoutId); 120 | reject(); 121 | }, 1000 * 30); // timeout after 30 seconds 122 | } 123 | ); 124 | }; 125 | 126 | waitForNewMemo().then( 127 | () => { 128 | const recentMsg = cache.messages; 129 | return res.json(recentMsg); 130 | } 131 | ).catch( 132 | () => { 133 | return res.json([]); // timeout, return empty array. 134 | } 135 | ); 136 | }, 137 | /* 138 | API: GET /api/list/old/:id 139 | params: 140 | id: message id 141 | description: Load messages older than given the given id 142 | */ 143 | old: (req, res) => { 144 | const id = req.params.id; 145 | 146 | // check id validity 147 | if(!mongoose.Types.ObjectId.isValid(id)) { 148 | return res.status(400).json({ 149 | error: { 150 | message: 'Invalid ID', 151 | code: 1 152 | } 153 | }); 154 | } 155 | 156 | 157 | Message.find({_id: { $lt: id}}) 158 | .sort({_id: -1}) 159 | .limit(25) 160 | .exec() 161 | .then( 162 | (messages) => { 163 | return res.json(messages.reverse()); 164 | } 165 | ).catch( 166 | (error) => { 167 | throw error; 168 | } 169 | ); 170 | 171 | }, 172 | /* 173 | API: GET /api/list/recent/:id 174 | params: 175 | id: message id 176 | description: Load messages newer than given the given id 177 | */ 178 | recent: (req, res) => { 179 | 180 | const id = req.params.id; 181 | const cache = req.app.get('cache'); 182 | 183 | // check id validity 184 | if(!mongoose.Types.ObjectId.isValid(id)) { 185 | return res.status(400).json({ 186 | error: { 187 | message: 'Invalid ID', 188 | code: 1 189 | } 190 | }); 191 | } 192 | 193 | let recentMsg = cache.getRecentMsg(id); 194 | 195 | 196 | if(typeof recentMsg === 'undefined') { 197 | return res.status(400).json({ 198 | error: { 199 | message: 'Invalid ID', 200 | code: 1 201 | } 202 | }); 203 | } 204 | 205 | if(recentMsg.length>0) { 206 | return res.json(recentMsg); 207 | } 208 | 209 | /* if recentMsg is undefined (which means given id does not exist in 210 | the cache), load directly from the mongod database. 211 | 212 | if(typeof recentMsg === 'undefined') { 213 | Message.find({_id: { $gt: id}}) 214 | .sort({_id: -1}) 215 | .limit(20) 216 | .exec() 217 | .then( 218 | (messages) => { 219 | return res.json(messages.reverse()); 220 | } 221 | ).catch( 222 | (error) => { 223 | throw error; 224 | } 225 | ); 226 | } else { 227 | // if there is more than one message, respond to the client 228 | if(recentMsg.length > 0) { 229 | return res.json(recentMsg); 230 | } 231 | }*/ 232 | 233 | 234 | /* if the tail matches id, it means there is no new memo. In this case, 235 | wait until there is a new memo. When 30 seconds pass, just return 236 | an empty array */ 237 | 238 | if(cache.tail === id) { 239 | const waitForNewMemo = () => { 240 | return new Promise( 241 | (resolve, reject) => { 242 | let timeoutId; 243 | const check = () => { 244 | if(id !== cache.tail) { 245 | // if the head is different 246 | resolve(); // resolve the Promise 247 | return; 248 | } 249 | timeoutId = setTimeout(check, 5); // or else, repeat this 250 | }; 251 | check(); 252 | setTimeout( 253 | () => { 254 | clearTimeout(timeoutId); 255 | reject(); 256 | }, 1000 * 30); // timeout after 30 seconds 257 | } 258 | ); 259 | }; 260 | 261 | waitForNewMemo().then( 262 | () => { 263 | recentMsg = cache.getRecentMsg(id); 264 | return res.json(recentMsg); 265 | } 266 | ).catch( 267 | () => { 268 | return res.json([]); // timeout, return empty array. 269 | } 270 | ); 271 | } 272 | 273 | } 274 | } 275 | }; 276 | -------------------------------------------------------------------------------- /server/src/controllers/session.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creates Random Color 3 | * @return {Array} RGB value in a array format: [r,g,b] 4 | */ 5 | function generateColor() { 6 | const r = Math.floor((Math.random() * 255)); 7 | const g = Math.floor((Math.random() * 255)); 8 | const b = Math.floor((Math.random() * 255)); 9 | return [r,g,b]; 10 | } 11 | 12 | /* 13 | API: GET / 14 | description: Returns or creates a session, which is in a random color format 15 | */ 16 | const session = (req, res) => { 17 | const session = req.session; 18 | // if color is not set, set a new color 19 | if(!session.color) { 20 | session.color = generateColor(); 21 | } 22 | 23 | // return the color 24 | return res.json({ 25 | color: session.color 26 | }); 27 | }; 28 | 29 | export default session; 30 | -------------------------------------------------------------------------------- /server/src/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import bodyParser from 'body-parser'; 3 | import morgan from 'morgan'; 4 | import session from 'express-session'; 5 | import compression from 'compression'; 6 | 7 | import mongoose from 'mongoose'; 8 | import connectMongo from 'connect-mongo'; 9 | 10 | import api from './routes'; 11 | import cors from 'cors'; 12 | 13 | import MessageCache from './utils/MessageCache'; 14 | 15 | import dotenv from 'dotenv'; 16 | 17 | import path from 'path'; 18 | 19 | // LOAD ENV CONFIG 20 | dotenv.config(); 21 | 22 | const app = express(); 23 | const port = process.env.PORT || 3000; 24 | 25 | 26 | const MongoStore = connectMongo(session); 27 | 28 | // SETUP MIDDLEWARE 29 | app.use(bodyParser.json()); 30 | app.use(morgan('tiny')); 31 | app.use(session({ 32 | secret: '@#@$MYSIGN#@$#$', 33 | resave: false, 34 | saveUninitialized: true, 35 | cookie: { 36 | maxAge: 14 * 24 * 60 * 60 * 1000 // 14 DAYS 37 | }, 38 | store: new MongoStore({ 39 | mongooseConnection: mongoose.connection, 40 | ttl:14 * 24 * 60 * 60 41 | }) 42 | })); 43 | app.use(cors()); 44 | app.use(compression()); 45 | 46 | 47 | // SERVE STATIC FILES 48 | app.use('/', express.static(path.join(__dirname, './../../client/public'))); 49 | 50 | 51 | // SETUP ROUTER 52 | app.use('/api', api); 53 | 54 | /* handle error */ 55 | app.use((err, req, res, next) => { 56 | console.error(err.stack); 57 | res.status(500).json({ 58 | error: { 59 | message: 'Something Broke!', 60 | code: 0 61 | } 62 | }); 63 | next(); 64 | }); 65 | 66 | const cache = new MessageCache(); 67 | app.set('cache', cache); 68 | 69 | mongoose.Promise = global.Promise; 70 | const db = mongoose.connection; 71 | db.on('error', console.error); 72 | db.once('open', () => { 73 | console.log('Connected to mongod server'); 74 | cache.startWorker(); 75 | }); 76 | 77 | mongoose.connect(process.env.DB_URI); 78 | 79 | app.listen(port, () => { 80 | console.log(`Express is running on port ${port}`); 81 | }); 82 | -------------------------------------------------------------------------------- /server/src/models/message.js: -------------------------------------------------------------------------------- 1 | import mongoose, { Schema } from 'mongoose'; 2 | 3 | const Message = new Schema({ 4 | message: String, 5 | date: { type: Date, default: Date.now }, 6 | uid: String, 7 | color: [] // stores color in r, g, b array 8 | }); 9 | 10 | export default mongoose.model('message', Message); 11 | -------------------------------------------------------------------------------- /server/src/routes/index.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import message from './message'; 3 | import session from './session'; 4 | 5 | const router = express.Router(); 6 | 7 | router.use('/*', (req, res, next) => { 8 | res.set("Connection", "keep-alive"); 9 | next(); 10 | }); 11 | router.use('/message', message); 12 | router.use('/session', session); 13 | 14 | export default router; 15 | -------------------------------------------------------------------------------- /server/src/routes/message.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { message } from '../controllers'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', message.list.initial); 7 | router.get('/old/:id', message.list.old); 8 | router.get('/recent', message.list.initRecent); 9 | router.get('/recent/:id', message.list.recent); 10 | router.post('/', message.write); 11 | 12 | export default router; 13 | -------------------------------------------------------------------------------- /server/src/routes/session.js: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import { session } from '../controllers'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/', session); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /server/src/utils/MessageCache.js: -------------------------------------------------------------------------------- 1 | import Message from '../models/message'; 2 | 3 | class MessageCache { 4 | 5 | constructor() { 6 | this.messages = []; 7 | this.idMap = []; 8 | this.tail = ''; 9 | this.pending = true; // to do initial loading 10 | this.timeoutId = undefined; 11 | } 12 | 13 | notifyWorker() { 14 | this.pending = true; 15 | } 16 | 17 | startWorker() { 18 | const loadData = () => { 19 | return Message.find() 20 | .sort({_id: -1}) 21 | .limit(25) 22 | .exec() 23 | .then( 24 | (messages) => { 25 | if(messages.length == 0) { 26 | return false; 27 | } 28 | 29 | messages.reverse(); // reverse the array, since queried result is in reversed order 30 | this.messages = messages; 31 | this.idMap = messages.map( 32 | (msg) => { 33 | return msg._id.toString(); 34 | } 35 | ); 36 | this.tail = this.idMap[messages.length-1]; 37 | return true; 38 | } 39 | ).catch( 40 | (error) => { 41 | console.error(error.stack); 42 | return false; 43 | } 44 | ); 45 | }; 46 | 47 | const work = () => { 48 | if(this.pending) { 49 | console.log("Data Loading.."); 50 | this.pending = false; 51 | loadData().then( 52 | (success) => { 53 | if(!success) { 54 | console.error('Data loading has failed, retry in 3 seconds'); 55 | this.pending = true; 56 | this.timeoutId = setTimeout(work, 3000); 57 | return; 58 | } 59 | console.log('Cache is updated - ', this.messages[0]._id); 60 | this.timeoutId = setTimeout(work, 5); 61 | } 62 | ); 63 | 64 | return; 65 | } 66 | 67 | this.timeoutId = setTimeout(work, 5); 68 | }; 69 | 70 | work(); 71 | } 72 | 73 | getRecentMsg(id) { 74 | 75 | const index = this.idMap.indexOf(id); 76 | 77 | // do not use cache when id does not exist 78 | if(this.idMap.indexOf(id) == -1) 79 | return undefined; 80 | 81 | return this.messages.slice(index+1, this.messages.length); 82 | } 83 | } 84 | 85 | export default MessageCache; 86 | --------------------------------------------------------------------------------