├── .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 |
--------------------------------------------------------------------------------