├── Procfile ├── app ├── src │ ├── components │ │ ├── sidebar │ │ │ ├── chat_rooms_list.jsx │ │ │ └── users_list.jsx │ │ ├── friends │ │ │ ├── friends_index.jsx │ │ │ └── friends_list.jsx │ │ ├── nav-bar │ │ │ ├── notifications.jsx │ │ │ ├── options.jsx │ │ │ ├── nav_bar.jsx │ │ │ └── user_search.jsx │ │ ├── user │ │ │ ├── search │ │ │ │ ├── users_search_index.jsx │ │ │ │ └── users_search_results_page.jsx │ │ │ ├── users_index.jsx │ │ │ └── user_home_page.jsx │ │ ├── main │ │ │ ├── footer.jsx │ │ │ └── chatterbox_app.jsx │ │ ├── chat │ │ │ └── chat_room.jsx │ │ └── auth │ │ │ ├── sign_up_page.jsx │ │ │ ├── login_page.jsx │ │ │ ├── login_form.jsx │ │ │ └── sign_up_form.jsx │ ├── constants │ │ ├── search_constants.js │ │ ├── current_user_constants.js │ │ └── user_constants.js │ ├── dispatcher │ │ └── dispatcher.js │ ├── utilities │ │ ├── flash.js │ │ └── auth.js │ ├── actions │ │ ├── search_actions.js │ │ ├── current_user_actions.js │ │ └── user_actions.js │ ├── stores │ │ ├── user_autocomplete_store.js │ │ ├── current_user_store.js │ │ └── logged_in_users_store.js │ ├── apiutil │ │ ├── api_session_util.js │ │ └── api_user_util.js │ └── main.js ├── static │ ├── stylesheets │ │ └── sass │ │ │ ├── modules │ │ │ ├── _mixins.scss │ │ │ ├── _all.scss │ │ │ ├── _clearfix.scss │ │ │ └── _variables.scss │ │ │ ├── partials │ │ │ ├── _links.scss │ │ │ ├── _icons.scss │ │ │ ├── _headers.scss │ │ │ ├── _base.scss │ │ │ ├── _body.scss │ │ │ ├── _input_box.scss │ │ │ ├── _flash.scss │ │ │ ├── _reset.scss │ │ │ └── _buttons.scss │ │ │ ├── components │ │ │ ├── sidebar │ │ │ │ ├── _users_index.scss │ │ │ │ └── _users_list.scss │ │ │ ├── _main_app.scss │ │ │ ├── navbar │ │ │ │ ├── _options.scss │ │ │ │ ├── _navbar.scss │ │ │ │ └── _user_search.scss │ │ │ ├── _footer.scss │ │ │ ├── _login_page.scss │ │ │ └── _sign_up_page.scss │ │ │ └── main.scss │ ├── favicon.ico │ ├── images │ │ ├── icons │ │ │ ├── ephraim.png │ │ │ ├── github.png │ │ │ └── linkedin.png │ │ ├── avatar_placeholder.png │ │ └── chatterbox_logo_angelic_version_by_spartasaurus.png │ ├── page_not_found.html │ └── index.html ├── api │ ├── utilities │ │ ├── __init__.py │ │ └── controller_utilities.py │ ├── controllers │ │ ├── json │ │ │ ├── __init__.py │ │ │ └── user_controller.py │ │ ├── __init__.py │ │ ├── static_pages_controller.py │ │ ├── application_controller.py │ │ ├── socket_controller.py │ │ └── session_controller.py │ ├── models │ │ ├── validations │ │ │ ├── __init__.py │ │ │ └── custom_validations.py │ │ ├── __init__.py │ │ ├── forms │ │ │ ├── __init__.py │ │ │ ├── login_form.py │ │ │ └── registration_form.py │ │ ├── message.py │ │ ├── session.py │ │ └── user.py │ └── __init__.py └── __init__.py ├── run_app ├── .gitignore ├── run_websockets ├── requirements.txt ├── webpack.config.js ├── package.json └── readme.md /Procfile: -------------------------------------------------------------------------------- 1 | ./run_app 2 | -------------------------------------------------------------------------------- /app/src/components/sidebar/chat_rooms_list.jsx: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/modules/_mixins.scss: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /app/api/utilities/__init__.py: -------------------------------------------------------------------------------- 1 | from controller_utilities import * 2 | -------------------------------------------------------------------------------- /app/api/controllers/json/__init__.py: -------------------------------------------------------------------------------- 1 | from user_controller import * 2 | -------------------------------------------------------------------------------- /app/api/models/validations/__init__.py: -------------------------------------------------------------------------------- 1 | from custom_validations import * 2 | -------------------------------------------------------------------------------- /app/api/models/__init__.py: -------------------------------------------------------------------------------- 1 | from user import User 2 | from message import Message 3 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_links.scss: -------------------------------------------------------------------------------- 1 | a { 2 | cursor: pointer; 3 | } 4 | -------------------------------------------------------------------------------- /app/api/__init__.py: -------------------------------------------------------------------------------- 1 | from app.api.controllers import * 2 | from app.api.models import * 3 | -------------------------------------------------------------------------------- /app/api/models/forms/__init__.py: -------------------------------------------------------------------------------- 1 | from registration_form import * 2 | from login_form import * 3 | -------------------------------------------------------------------------------- /app/src/constants/search_constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | RECEIVE_USERS: "RECEIVE_USERS" 3 | }; 4 | -------------------------------------------------------------------------------- /app/static/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/favicon.ico -------------------------------------------------------------------------------- /app/static/stylesheets/sass/modules/_all.scss: -------------------------------------------------------------------------------- 1 | @import "clearfix"; 2 | @import "mixins"; 3 | @import "variables"; 4 | -------------------------------------------------------------------------------- /run_app: -------------------------------------------------------------------------------- 1 | #!venv/bin/python 2 | 3 | from app import app 4 | 5 | if __name__ == '__main__': 6 | app.run(debug=True) 7 | -------------------------------------------------------------------------------- /app/api/utilities/controller_utilities.py: -------------------------------------------------------------------------------- 1 | def build_user_response_object(user): 2 | return { "username": user.username } 3 | -------------------------------------------------------------------------------- /app/static/images/icons/ephraim.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/icons/ephraim.png -------------------------------------------------------------------------------- /app/static/images/icons/github.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/icons/github.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | tmp/ 3 | venv/ 4 | manage.py 5 | .DS_Store 6 | node_modules 7 | .sass-cache 8 | *.log 9 | *.yaml 10 | -------------------------------------------------------------------------------- /app/static/images/icons/linkedin.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/icons/linkedin.png -------------------------------------------------------------------------------- /app/static/images/avatar_placeholder.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/avatar_placeholder.png -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/sidebar/_users_index.scss: -------------------------------------------------------------------------------- 1 | .users-index { 2 | color: white; 3 | text-align: center; 4 | } 5 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/modules/_clearfix.scss: -------------------------------------------------------------------------------- 1 | .group:after { 2 | content: ""; 3 | display: block; 4 | clear: both; 5 | } 6 | -------------------------------------------------------------------------------- /run_websockets: -------------------------------------------------------------------------------- 1 | #!venv/bin/python 2 | 3 | from app import app, socketio 4 | 5 | if __name__ == '__main__': 6 | socketio.run(app) 7 | -------------------------------------------------------------------------------- /app/src/dispatcher/dispatcher.js: -------------------------------------------------------------------------------- 1 | import { Dispatcher } from 'flux'; 2 | const AppDispatcher = new Dispatcher(); 3 | export default AppDispatcher; 4 | -------------------------------------------------------------------------------- /app/src/constants/current_user_constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | RECEIVE_CURRENT_USER: "RECEIVE_CURRENT_USER", 3 | LOG_OUT_CURRENT_USER: "LOG_OUT_CURRENT_USER" 4 | }; 5 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/_main_app.scss: -------------------------------------------------------------------------------- 1 | #wrapper { 2 | min-height: 100vh; 3 | position:relative; 4 | } 5 | 6 | #content { 7 | padding-bottom: 73px; 8 | } 9 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_icons.scss: -------------------------------------------------------------------------------- 1 | .social-media-icon { 2 | width: $icon-width; 3 | height: $icon-height; 4 | border-radius: $icon-border-radius; 5 | } 6 | -------------------------------------------------------------------------------- /app/api/controllers/__init__.py: -------------------------------------------------------------------------------- 1 | from application_controller import * 2 | from session_controller import * 3 | from static_pages_controller import * 4 | from socket_controller import * 5 | -------------------------------------------------------------------------------- /app/static/images/chatterbox_logo_angelic_version_by_spartasaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ephraimpei/chatterbox/HEAD/app/static/images/chatterbox_logo_angelic_version_by_spartasaurus.png -------------------------------------------------------------------------------- /app/src/constants/user_constants.js: -------------------------------------------------------------------------------- 1 | export default { 2 | RECEIVE_USERS: "RECEIVE_USERS", 3 | RECEIVE_SINGLE_USER: "RECEIVE_SINGLE_USER", 4 | RECEIVE_LOGGED_IN_USERS: "RECEIVE_LOGGED_IN_USERS" 5 | }; 6 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_headers.scss: -------------------------------------------------------------------------------- 1 | h1 { 2 | font-size: $h1-font-size; 3 | font-weight: $bold; 4 | } 5 | 6 | h2 { 7 | font-size: $h2-font-size; 8 | font-weight: $bold; 9 | } 10 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_base.scss: -------------------------------------------------------------------------------- 1 | @import "./body"; 2 | @import "./buttons"; 3 | @import "./headers"; 4 | @import "./flash"; 5 | @import "./icons"; 6 | @import "./input_box"; 7 | @import "./links"; 8 | -------------------------------------------------------------------------------- /app/static/page_not_found.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | You must be in the wrong place because... PAGE NOT FOUND. 9 | 10 | 11 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/sidebar/_users_list.scss: -------------------------------------------------------------------------------- 1 | .users-list { 2 | background: $users-list-background; 3 | height: $users-list-height; 4 | width: $users-list-width; 5 | opacity: $users-list-opacity; 6 | } 7 | 8 | @import 'users_index' 9 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_body.scss: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: $base-font-family; 3 | font-weight: $base-font-weight; 4 | font-size: $base-font-size; 5 | line-height: $base-line-height; 6 | background: $body-bg; 7 | height: 100%; 8 | } 9 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/navbar/_options.scss: -------------------------------------------------------------------------------- 1 | .options { 2 | .options-list { 3 | display: none; 4 | position: absolute; 5 | cursor: pointer; 6 | } 7 | } 8 | 9 | .options:hover { 10 | .options-list { 11 | display: block; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /app/src/utilities/flash.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | let displayFlashMessage = function(message) { 4 | $('#flash').text(message); 5 | 6 | $('#flash').delay(500).fadeIn('normal', function() { 7 | $(this).delay(2500).fadeOut(); 8 | }); 9 | }; 10 | 11 | export { displayFlashMessage }; 12 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_input_box.scss: -------------------------------------------------------------------------------- 1 | input { 2 | padding: $input-box-padding; 3 | border-radius: $input-box-border-radius; 4 | font-size: $base-font-size; 5 | } 6 | 7 | input.invalid { 8 | border: $failed-validation-input-border; 9 | box-shadow: $failed-validation-box-shadow; 10 | } 11 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_flash.scss: -------------------------------------------------------------------------------- 1 | #flash { 2 | display: none; 3 | position: absolute; 4 | top: $flash-top; 5 | left: $flash-left; 6 | font-size: $flash-font-size; 7 | border: $flash-border; 8 | border-radius: $flash-border-radius; 9 | background: $flash-background-color; 10 | padding: $flash-padding; 11 | z-index: $flash-z-index; 12 | } 13 | -------------------------------------------------------------------------------- /app/src/components/friends/friends_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class FriendsIndex extends React.Component { 4 | constructor (props, context) { 5 | super(props, context); 6 | } 7 | 8 | render () { 9 | return ( 10 |
11 |
12 | ); 13 | } 14 | } 15 | 16 | export default FriendsIndex; 17 | -------------------------------------------------------------------------------- /app/src/components/nav-bar/notifications.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Notifications extends React.Component { 4 | constructor (props, context) { 5 | super(props, context); 6 | } 7 | 8 | render () { 9 | return ( 10 |
Notifications 11 |
12 | ); 13 | } 14 | } 15 | 16 | export default Notifications; 17 | -------------------------------------------------------------------------------- /app/static/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Chatterbox 5 | 6 | 7 | 8 |
9 |
10 |
11 | 12 |
13 | 14 | 15 | -------------------------------------------------------------------------------- /app/src/components/user/search/users_search_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class UsersSearchIndex extends React.Component { 4 | constructor (props, context) { 5 | super(props, context); 6 | } 7 | 8 | render () { 9 | return ( 10 |
11 | 12 |
13 | ); 14 | } 15 | } 16 | 17 | export default UsersSearchIndex; 18 | -------------------------------------------------------------------------------- /app/api/models/message.py: -------------------------------------------------------------------------------- 1 | from flask import url_for 2 | from app import db 3 | import datetime 4 | 5 | class Message(db.EmbeddedDocument): 6 | created_at = db.DateTimeField(default=datetime.datetime.now, required=True) 7 | sender = db.StringField(max_length=255, required=True) 8 | receiver = db.StringField(max_length=255, required=True) 9 | body = db.StringField(max_length=255, required=True) 10 | -------------------------------------------------------------------------------- /app/src/actions/search_actions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../dispatcher/dispatcher.js'; 2 | import SearchConstants from "../constants/search_constants.js"; 3 | 4 | export default new class { 5 | receiveUsers (users, isAutoCompleteSelection) { 6 | AppDispatcher.dispatch({ 7 | actionType: SearchConstants.RECEIVE_USERS, 8 | users: users, 9 | isAutoCompleteSelection: isAutoCompleteSelection 10 | }); 11 | } 12 | }(); 13 | -------------------------------------------------------------------------------- /app/src/components/friends/friends_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import FriendsIndex from "./friends_index.jsx"; 3 | 4 | class FriendsList extends React.Component { 5 | constructor (props, context) { 6 | super(props, context); 7 | } 8 | 9 | render () { 10 | return ( 11 |
12 | 13 |
14 | ); 15 | } 16 | } 17 | 18 | export default FriendsList; 19 | -------------------------------------------------------------------------------- /app/src/components/user/users_index.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class UsersIndex extends React.Component { 4 | constructor (props, context) { 5 | super(props, context); 6 | } 7 | 8 | render () { 9 | const users = this.props.users.map((user, idx) =>
  • { user }
  • ); 10 | 11 | return ( 12 | 15 | ); 16 | } 17 | } 18 | 19 | export default UsersIndex; 20 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/main.scss: -------------------------------------------------------------------------------- 1 | // utilities (clearfix, mixins, variables) 2 | @import "./modules/all"; 3 | 4 | // reset css 5 | @import "./partials/reset"; 6 | 7 | // base 8 | @import "./partials/base"; 9 | 10 | // components 11 | @import "./components/footer"; 12 | @import "./components/sidebar/users_list"; 13 | @import "./components/login_page"; 14 | @import "./components/main_app"; 15 | @import "./components/navbar/navbar"; 16 | @import "./components/sign_up_page"; 17 | -------------------------------------------------------------------------------- /app/api/models/forms/login_form.py: -------------------------------------------------------------------------------- 1 | from wtforms import Form, StringField, PasswordField, validators 2 | from app.api.models.validations import check_if_username_exists, validate_user_credentials 3 | 4 | class LoginForm(Form): 5 | username = StringField('username', [ 6 | validators.Required(), 7 | check_if_username_exists 8 | ]) 9 | password = PasswordField('password', [ 10 | validators.Required(), 11 | validate_user_credentials 12 | ]) 13 | -------------------------------------------------------------------------------- /app/__init__.py: -------------------------------------------------------------------------------- 1 | import eventlet 2 | from flask import Flask 3 | from flask.ext.mongoengine import MongoEngine 4 | from flask.ext.socketio import SocketIO 5 | 6 | app = Flask(__name__, static_url_path="", static_folder="static") 7 | socketio = SocketIO(app) 8 | 9 | app.config["MONGODB_SETTINGS"] = {'DB': "chatterbox_test"} 10 | app.config["SECRET_KEY"] = "whatitdo" 11 | 12 | db = MongoEngine(app) 13 | 14 | eventlet.monkey_patch() 15 | 16 | from app.api.controllers import * 17 | from app.api.controllers.json import * 18 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | certifi==2015.11.20.1 2 | enum34==1.1.2 3 | eventlet==0.18.1 4 | Flask==0.10.1 5 | flask-mongoengine==0.7.4 6 | Flask-Script==2.0.5 7 | Flask-SocketIO==2.0 8 | Flask-WTF==0.12 9 | greenlet==0.4.9 10 | itsdangerous==0.24 11 | Jinja2==2.8 12 | MarkupSafe==0.23 13 | mongoengine==0.10.5 14 | pathlib==1.0.1 15 | py-bcrypt==0.4 16 | pymongo==3.2 17 | python-engineio==0.8.7 18 | python-socketio==1.0 19 | simplejson==3.8.1 20 | six==1.10.0 21 | urllib3==1.13.1 22 | Werkzeug==0.11.3 23 | wheel==0.24.0 24 | WTForms==2.1 25 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_reset.scss: -------------------------------------------------------------------------------- 1 | html, body, h1, h2, h3, div, footer, ul, li, a, figure, button, textarea, form, label { 2 | padding: 0; 3 | border: 0; 4 | margin: 0; 5 | 6 | font: inherit; 7 | vertical-align: inherit; 8 | text-align: inherit; 9 | text-decoration: inherit; 10 | color: inherit; 11 | 12 | background: transparent 13 | } 14 | 15 | ul { 16 | list-style: none; 17 | } 18 | 19 | input, textarea { 20 | outline: 0; 21 | } 22 | 23 | img { 24 | display: block; 25 | width: 100%; 26 | height: auto; 27 | } 28 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/navbar/_navbar.scss: -------------------------------------------------------------------------------- 1 | .header { 2 | background: $header-background; 3 | border-bottom: $header-bottom-border; 4 | 5 | .nav-bar { 6 | width: $nav-bar-width; 7 | margin: auto; 8 | display: flex; 9 | align-items: center; 10 | justify-content: space-between; 11 | 12 | .logo { 13 | width: $logo-dimension; 14 | height: $logo-dimension; 15 | cursor: pointer; 16 | } 17 | } 18 | } 19 | 20 | .header.not-visible { 21 | display: none; 22 | } 23 | 24 | @import "user_search"; 25 | @import "options"; 26 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/partials/_buttons.scss: -------------------------------------------------------------------------------- 1 | button { 2 | padding: $button-padding; 3 | background: $button-background; 4 | font-size: $button-font-size; 5 | border: $button-border; 6 | border-radius: $button-border-radius; 7 | text-align: center; 8 | cursor: pointer; 9 | } 10 | 11 | button:focus { 12 | outline: 0; 13 | } 14 | 15 | button:active, button.disabled, button.pressed { 16 | text-shadow: 1px 1px 2px black; 17 | box-shadow: inset 0 0 0 1px #27496d, inset 0 5px 30px #193047; 18 | } 19 | 20 | button:hover { 21 | background: darken($button-background, 10%); 22 | } 23 | -------------------------------------------------------------------------------- /app/src/actions/current_user_actions.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../dispatcher/dispatcher.js'; 2 | import CurrentUserConstants from "../constants/current_user_constants.js"; 3 | 4 | export default new class { 5 | receiveCurrentUser (currentUser) { 6 | AppDispatcher.dispatch({ 7 | actionType: CurrentUserConstants.RECEIVE_CURRENT_USER, 8 | currentUser: currentUser 9 | }); 10 | } 11 | 12 | logoutCurrentUser (currentUser) { 13 | AppDispatcher.dispatch({ 14 | actionType: CurrentUserConstants.LOG_OUT_CURRENT_USER, 15 | currentUser: currentUser 16 | }); 17 | } 18 | }(); 19 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | entry: './app/src/main.js', 3 | output: { 4 | path: './app/static/javascripts', 5 | filename: 'bundle.js' 6 | }, 7 | module: { 8 | loaders: [{ 9 | test: /\.js$|\.jsx$/, 10 | exclude: /node_modules/, 11 | loader: 'babel', 12 | query: { 13 | presets: [ 'es2015', 'react', 'stage-0' ] 14 | } 15 | }, { 16 | test: /\.scss$/, 17 | exclude: /node_modules/, 18 | loaders: ["style", "css", "sass"] 19 | }] 20 | }, 21 | devtool: 'source-maps', 22 | resolve: { 23 | extensions: ['', '.js', '.jsx'] 24 | } 25 | }; 26 | -------------------------------------------------------------------------------- /app/api/models/forms/registration_form.py: -------------------------------------------------------------------------------- 1 | from wtforms import Form, StringField, PasswordField, validators 2 | from app.api.models.validations import check_if_username_taken 3 | 4 | class RegistrationForm(Form): 5 | username = StringField('username', [ 6 | validators.Required(), 7 | validators.Length(min=4, max=25), 8 | check_if_username_taken 9 | ]) 10 | password = PasswordField('password', [ 11 | validators.Required(), 12 | validators.EqualTo('confirm', message='Passwords must match'), 13 | validators.Length(min=4, max=25) 14 | ]) 15 | confirm = PasswordField('confirm') 16 | -------------------------------------------------------------------------------- /app/src/actions/user_actions.js: -------------------------------------------------------------------------------- 1 | import UserConstants from "../constants/user_constants.js"; 2 | import AppDispatcher from '../dispatcher/dispatcher.js'; 3 | 4 | export default new class { 5 | receiveUsers (users) { 6 | AppDispatcher.dispatch({ 7 | actionType: UserConstants.RECEIVE_USERS, 8 | users: users 9 | }); 10 | } 11 | 12 | receiveSingleUser (user) { 13 | AppDispatcher.dispatch({ 14 | actionType: UserConstants.RECEIVE_SINGLE_USER, 15 | user: user 16 | }); 17 | } 18 | 19 | receiveLoggedInUsers (users) { 20 | AppDispatcher.dispatch({ 21 | actionType: UserConstants.RECEIVE_LOGGED_IN_USERS, 22 | users: users 23 | }); 24 | } 25 | 26 | }(); 27 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/_footer.scss: -------------------------------------------------------------------------------- 1 | #footer-wrapper { 2 | background: $footer-bg; 3 | border-top: $standard-border; 4 | height: $footer-height; 5 | position: absolute; 6 | width: 100%; 7 | bottom: 0; 8 | left: 0; 9 | 10 | .footer { 11 | width: $footer-width; 12 | margin: $footer-margin; 13 | padding: $footer-padding; 14 | font-size: $footer-font-size; 15 | color: $footer-font-color; 16 | 17 | .about { 18 | margin-top: $about-me-margin-top; 19 | opacity: $about-me-opacity; 20 | float: left; 21 | } 22 | 23 | .links { 24 | float: right; 25 | 26 | a { 27 | margin-left: $icon-list-margin; 28 | display: inline-block; 29 | } 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/api/controllers/static_pages_controller.py: -------------------------------------------------------------------------------- 1 | from app import app 2 | from flask import send_from_directory 3 | 4 | @app.errorhandler(404) 5 | def page_not_found(error): 6 | return send_from_directory(app.static_folder, 'page_not_found.html'), 404 7 | 8 | @app.after_request 9 | def add_header(response): 10 | response.headers['X-UA-Compatible'] = 'IE=Edge,chrome=1' 11 | response.headers['Cache-Control'] = 'public, max-age=0' 12 | return response 13 | 14 | @app.route("/") 15 | @app.route("/users/new") 16 | @app.route("/users/edit") 17 | def index(): 18 | return send_from_directory(app.static_folder, "index.html") 19 | 20 | @app.route("/users/") 21 | def show_user_profile(username): 22 | return send_from_directory(app.static_folder, "index.html") 23 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/_login_page.scss: -------------------------------------------------------------------------------- 1 | .login-page { 2 | h1, h2 { text-align: center; } 3 | 4 | .login-form { 5 | width: $login-form-width; 6 | margin: auto; 7 | 8 | .login-form-wrapper { 9 | button, label, input { 10 | margin: $login-form-item-margin; 11 | } 12 | 13 | a { 14 | text-align: center; 15 | } 16 | 17 | a:hover { 18 | color: blue; 19 | text-decoration: underline; 20 | } 21 | 22 | label { 23 | text-align: center; 24 | } 25 | 26 | input { 27 | width: 100%; 28 | } 29 | 30 | display: flex; 31 | flex-direction: column; 32 | justify-content: center; 33 | margin: $login-form-margins; 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /app/api/controllers/application_controller.py: -------------------------------------------------------------------------------- 1 | from flask import session 2 | from app.api.models import User 3 | import pdb 4 | 5 | def current_user(cookie): 6 | if not session: 7 | return None 8 | else: 9 | for token in session: 10 | if cookie == token and User.find_by_session_token(token): 11 | return User.find_by_session_token(token)[0] 12 | return None 13 | 14 | def login(user): 15 | session[user.reset_session_token()] = user.username 16 | 17 | def logout(cookie): 18 | current_user(cookie).reset_session_token() 19 | session.pop(cookie, None) 20 | 21 | def logged_in_users(): 22 | users = [] 23 | 24 | for token in session: 25 | user = User.find_by_session_token(token) 26 | if user: 27 | users.append(user[0].username) 28 | 29 | return users 30 | -------------------------------------------------------------------------------- /app/static/stylesheets/sass/components/_sign_up_page.scss: -------------------------------------------------------------------------------- 1 | .sign-up-page { 2 | h1, h2 { text-align: center; } 3 | 4 | .sign-up-form { 5 | width: $sign-up-form-width; 6 | margin: auto; 7 | 8 | .sign-up-form-wrapper { 9 | * { 10 | margin: $sign-up-form-item-margin; 11 | } 12 | 13 | label, a { 14 | text-align: center; 15 | } 16 | 17 | a:hover { 18 | color: blue; 19 | text-decoration: underline; 20 | } 21 | 22 | input { 23 | width: 100%; 24 | } 25 | 26 | .form-avatar-preview { 27 | width: $avatar-preview-width; 28 | height: $avatar-preview-height; 29 | } 30 | 31 | display: flex; 32 | flex-direction: column; 33 | justify-content: center; 34 | margin: $sign-up-form-margins; 35 | } 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /app/api/models/validations/custom_validations.py: -------------------------------------------------------------------------------- 1 | from app.api.models import User 2 | from wtforms import ValidationError 3 | import pdb 4 | 5 | def check_if_username_taken(form, field): 6 | message = "Username already taken" 7 | 8 | if User.find_by_username(form.username.data).count() > 0: 9 | raise ValidationError(message) 10 | 11 | def check_if_username_exists(form, field): 12 | message = "Username not found" 13 | 14 | if User.find_by_username(form.username.data).count() == 0: 15 | raise ValidationError(message) 16 | 17 | def validate_user_credentials(form, field): 18 | if User.find_by_username(form.username.data).count() > 0: 19 | user = User.find_by_username(form.username.data)[0] 20 | else: 21 | return 22 | 23 | if not User.validate_user_credentials(user, form.password.data): 24 | message = "Invalid credentials" 25 | raise ValidationError(message) 26 | -------------------------------------------------------------------------------- /app/src/components/nav-bar/options.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ApiSessionUtil from '../../apiutil/api_session_util.js'; 3 | 4 | class Options extends React.Component { 5 | constructor (props, context) { 6 | super(props, context); 7 | this.logoutCurrentUser = this.logoutCurrentUser.bind(this); 8 | } 9 | 10 | logoutCurrentUser (e) { 11 | e.preventDefault(); 12 | 13 | ApiSessionUtil.logout(this.props.logoutSuccess); 14 | } 15 | render () { 16 | let options = ["Update profile", "Logout"]; 17 | 18 | let optionListItems = options.map( (option, idx) => 19 |
  • { option }
  • 20 | ); 21 | 22 | return ( 23 |
    Options 24 | 28 |
    29 | ); 30 | } 31 | } 32 | 33 | export default Options; 34 | -------------------------------------------------------------------------------- /app/src/utilities/auth.js: -------------------------------------------------------------------------------- 1 | import $ from 'jquery'; 2 | 3 | let failedAuthErrors = function (errors) { 4 | $(".submit").removeClass("disabled").prop("disabled", false); 5 | 6 | let [usernameErrors, passwordErrors] = [[], []]; 7 | 8 | errors.forEach (function (err) { 9 | switch (err[0]) { 10 | case "username": 11 | usernameErrors.push(err[1][0]); 12 | $(".form-username-input").addClass("invalid"); 13 | break; 14 | case "password": 15 | passwordErrors.push(err[1][0]); 16 | if (!$(".login-form-password-input").hasClass("invalid")) { 17 | $(".form-password-input").addClass("invalid"); 18 | } 19 | break; 20 | } 21 | }); 22 | 23 | return [usernameErrors, passwordErrors]; 24 | }; 25 | 26 | let removeInvalidClass = function (className) { 27 | if ($(`.${ className }`).hasClass("invalid")) { 28 | $(`.${ className }`).removeClass("invalid"); 29 | } 30 | }; 31 | 32 | export { failedAuthErrors, removeInvalidClass }; 33 | -------------------------------------------------------------------------------- /app/api/models/session.py: -------------------------------------------------------------------------------- 1 | from app import db 2 | import uuid 3 | 4 | class Session(db.Document): 5 | username = db.StringField(max_length=255, required=True) 6 | session_token = db.StringField(max_length=255, required=True) 7 | 8 | @classmethod 9 | def generate_session_token(cls): 10 | return str(uuid.uuid1()) 11 | 12 | @classmethod 13 | def find_by_session_token(cls, session_token): 14 | return User.objects(session_token = session_token) 15 | 16 | @classmethod 17 | def destroy(cls, session): 18 | if session.delete(): 19 | return True 20 | else: 21 | return False 22 | 23 | def reset_session_token(self): 24 | self.session_token = Session.generate_session_token() 25 | self.save() 26 | return self.session_token 27 | 28 | def __repr__(self): 29 | return ''.format(self.session_token, self.username) 30 | 31 | meta = { 32 | 'indexes': ['session_token'] 33 | } 34 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "chatterbox", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "run_app", 6 | "scripts": { 7 | "test": "echo \"Error: no test specified\" && exit 1", 8 | "start": "./run_app" 9 | }, 10 | "author": "", 11 | "license": "ISC", 12 | "dependencies": { 13 | "babel-core": "^6.3.26", 14 | "babel-loader": "^6.2.1", 15 | "babel-preset-es2015": "^6.3.13", 16 | "babel-preset-react": "^6.3.13", 17 | "babel-preset-stage-0": "^6.3.13", 18 | "css-loader": "^0.23.1", 19 | "eventemitter3": "^1.1.1", 20 | "file-loader": "^0.8.5", 21 | "flux": "^2.1.1", 22 | "history": "^1.17.0", 23 | "jquery": "^2.1.4", 24 | "node-sass": "^3.4.2", 25 | "react": "^0.14.5", 26 | "react-dom": "^0.14.5", 27 | "react-router": "^2.0.0-rc5", 28 | "sass-loader": "^3.1.2", 29 | "socket.io-client": "^1.4.5", 30 | "style-loader": "^0.13.0", 31 | "webpack": "^1.12.9" 32 | }, 33 | "jshintConfig": { 34 | "esnext": true 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | # Chatterbox 2 | 3 | WIP - Live Site coming 4 | 5 | ## Summary 6 | 7 | Chatterbox is a slack-like chat app which uses the latest technologies such as WebSockets to enable the chat feature. 8 | 9 | ### Languages 10 | 11 | * Python 12 | * JavaScript 13 | * HTML 14 | * SASS/CSS 15 | 16 | ### Frameworks 17 | 18 | * Flask 19 | * React and Flux 20 | 21 | ### Libraries and Technologies 22 | 23 | * Node technologies 24 | + Webpack 25 | + jQuery 26 | + ReactRouter 27 | * Python technologies 28 | + MongoEngine 29 | + BCrypt 30 | + Flask-Script 31 | + Flask-WTForms 32 | + Flask-SocketIO 33 | * DB technologies 34 | + MongoDB 35 | 36 | ### App features 37 | 38 | - Incorporates latest JavaScript ES6 features and syntax 39 | - Webpack used to manage JavaScript module dependencies and to load ES6 and SASS files 40 | - Logging in establishes client to server WebSocket connection 41 | - Global chat room and private, peer to peer, chat rooms supported 42 | - Stores user and chat messages in MongoDB (user credentials hashed with BCrypt) 43 | -------------------------------------------------------------------------------- /app/src/components/main/footer.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class Footer extends React.Component { 4 | constructor () { 5 | super(); 6 | } 7 | 8 | render () { 9 | return ( 10 |
    11 | 12 | 13 | 30 |
    31 | ); 32 | } 33 | } 34 | 35 | export default Footer; 36 | -------------------------------------------------------------------------------- /app/src/components/nav-bar/nav_bar.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UserSearch from './user_search.jsx'; 3 | import Notifications from './notifications.jsx'; 4 | import Options from './options.jsx'; 5 | import { displayFlashMessage } from '../../utilities/flash.js'; 6 | 7 | class NavBar extends React.Component { 8 | constructor (props, context) { 9 | super(props, context); 10 | this.logoutSuccess = this.logoutSuccess.bind(this); 11 | } 12 | 13 | logoutSuccess (message) { 14 | displayFlashMessage(message); 15 | } 16 | 17 | render () { 18 | return ( 19 |
    20 |
    21 | 23 | 24 | 25 | 26 |
    27 |
    28 | ); 29 | } 30 | } 31 | 32 | export default NavBar; 33 | -------------------------------------------------------------------------------- /app/src/components/chat/chat_room.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | class ChatRoom extends React.Component { 4 | constructor (props, context) { 5 | super(props, contextTypes); 6 | this.changeMessage = this.changeMessage.bind(this); 7 | this.handleKeyPress = tihs.handleKeyPress.bind(this); 8 | this.handleMessageSubmission = this.handleMessageSubmission.bind(this); 9 | } 10 | 11 | handleKeyPress (e) { 12 | if (e.charCode === 13) { this.handleMessageSubmission(); } 13 | } 14 | 15 | handleMessageSubmission (e) { 16 | socket.emit('global chat request', { data: e.currentTarget.value }); 17 | } 18 | 19 | render () { 20 | return ( 21 |
    22 |
    23 |
    26 | 27 | 28 |
    29 |
    30 | ); 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /app/api/controllers/socket_controller.py: -------------------------------------------------------------------------------- 1 | from flask import request 2 | # from flask import session, jsonify 3 | from flask.ext.socketio import emit, join_room, leave_room, close_room, rooms, disconnect 4 | from app import app, socketio 5 | from app.api.controllers import application_controller 6 | import pdb 7 | 8 | @socketio.on('private chat request', namespace='/chat') 9 | def private_message(message): 10 | emit('private chat response', {'data': message['data']}) 11 | 12 | @socketio.on('global chat request', namespace='/chat') 13 | def global_message(message): 14 | emit('global chat response', {'data': message['data']}, broadcast=True) 15 | 16 | @socketio.on('connect', namespace='/chat') 17 | def connect(): 18 | username = request.args.get('username') 19 | emit('a user connected', { 'broadcast': "{0} connected".format(username), 20 | 'username': username}, broadcast=True) 21 | 22 | @socketio.on('disconnect', namespace='/chat') 23 | def disconnect(): 24 | username = request.args.get('username') 25 | emit('a user disconnected', { 'broadcast': "{0} disconnected".format(username), 26 | 'username': username}, broadcast=True) 27 | print('Client disconnected', request.sid) 28 | -------------------------------------------------------------------------------- /app/src/stores/user_autocomplete_store.js: -------------------------------------------------------------------------------- 1 | import AppDispatcher from '../dispatcher/dispatcher.js'; 2 | import SearchConstants from "../constants/search_constants.js"; 3 | import EventEmitter from 'eventemitter3'; 4 | 5 | const CHANGE_EVENT = "change"; 6 | 7 | class UserSearchAutoCompleteStore extends EventEmitter { 8 | constructor () { 9 | super(); 10 | this.users = []; 11 | } 12 | 13 | addChangeListener (callback) { 14 | this.on(CHANGE_EVENT, callback); 15 | } 16 | 17 | removeChangeListener (callback) { 18 | this.removeListener(CHANGE_EVENT, callback); 19 | } 20 | 21 | get () { 22 | return this.users.slice(); 23 | } 24 | 25 | set (users) { 26 | this.users = users; 27 | } 28 | } 29 | 30 | const userSearchAutoCompleteStore = new UserSearchAutoCompleteStore(); 31 | 32 | AppDispatcher.register(function (payload) { 33 | switch (payload.actionType) { 34 | case SearchConstants.RECEIVE_USERS: 35 | userSearchAutoCompleteStore.set(payload.users); 36 | 37 | if (!payload.isAutoCompleteSelection) { 38 | userSearchAutoCompleteStore.emit(CHANGE_EVENT); 39 | } 40 | break; 41 | } 42 | }); 43 | 44 | export default userSearchAutoCompleteStore; 45 | -------------------------------------------------------------------------------- /app/src/apiutil/api_session_util.js: -------------------------------------------------------------------------------- 1 | import $ from "jquery"; 2 | import CurrentUserActions from "../actions/current_user_actions.js"; 3 | 4 | class ApiSessionUtil { 5 | login(formData, success, failure) { 6 | const receiveCurrentUser = (data) => { 7 | CurrentUserActions.receiveCurrentUser(data.user); 8 | success(data.message, data.user.username); 9 | }; 10 | 11 | const receiveErrors = (data) => { 12 | failure(data.responseJSON.errors); 13 | }; 14 | 15 | $.ajax({ 16 | url: "/session", 17 | method: "POST", 18 | processData: false, 19 | contentType: false, 20 | dataType: "json", 21 | data: formData 22 | }).done(receiveCurrentUser).fail(receiveErrors); 23 | } 24 | 25 | logout(success) { 26 | $.ajax({ 27 | url: '/session', 28 | method: 'DELETE', 29 | dataType: 'json', 30 | success: (data) => { 31 | CurrentUserActions.logoutCurrentUser(data.user); 32 | success(data.message); 33 | } 34 | }); 35 | } 36 | 37 | fetchCurrentUser() { 38 | const receiveCurrentUser = (data) => CurrentUserActions.receiveCurrentUser(data.user); 39 | 40 | $.get('/session', receiveCurrentUser); 41 | } 42 | } 43 | 44 | export default new ApiSessionUtil(); 45 | -------------------------------------------------------------------------------- /app/src/components/user/search/users_search_results_page.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import currentUserStore from '../../../stores/current_user_store.js'; 3 | import NavBar from '../../nav-bar/nav_bar.jsx'; 4 | import UsersSearchIndex from './users_search_index.jsx'; 5 | 6 | class UsersSearchResultsPage extends React.Component { 7 | constructor (props, context) { 8 | super(props, context); 9 | this._ensureLoggedIn = this.__ensureLoggedIn.bind(this); 10 | } 11 | 12 | static contextTypes = { 13 | router: React.PropTypes.object.isRequired 14 | } 15 | 16 | componentWillMount () { 17 | this._ensureLoggedIn(); 18 | } 19 | 20 | componentDidMount () { 21 | currentUserStore.addChangeListener(this._ensureLoggedIn); 22 | } 23 | 24 | componentWillUnmount () { 25 | currentUserStore.removeChangeListener(this._ensureLoggedIn); 26 | } 27 | 28 | __ensureLoggedIn () { 29 | if (!currentUserStore.isLoggedIn()) { this.context.router.push('/'); } 30 | } 31 | 32 | render () { 33 | return ( 34 |
    35 |
    36 | 37 |
    38 |
    39 | ); 40 | } 41 | } 42 | 43 | export default UsersSearchResultsPage; 44 | -------------------------------------------------------------------------------- /app/src/components/sidebar/users_list.jsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import UsersIndex from '../user/users_index.jsx'; 3 | import ApiUserUtil from '../../apiutil/api_user_util.js'; 4 | import currentUserStore from '../../stores/current_user_store.js'; 5 | import loggedInUsersStore from '../../stores/logged_in_users_store.js'; 6 | 7 | class UsersList extends React.Component { 8 | constructor (props, context) { 9 | super(props, context); 10 | this._onChange = this._onChange.bind(this); 11 | this.state = { loggedInUsers: loggedInUsersStore.get() }; 12 | } 13 | 14 | componentWillMount () { 15 | const username = currentUserStore.get().username; 16 | ApiUserUtil.fetchUsers(username, "loggedinusers"); 17 | } 18 | 19 | componentDidMount () { 20 | loggedInUsersStore.addChangeListener(this._onChange); 21 | } 22 | 23 | componentWillUnmount () { 24 | loggedInUsersStore.removeChangeListener(this._onChange); 25 | } 26 | 27 | _onChange () { 28 | this.setState({ loggedInUsers: loggedInUsersStore.get() }); 29 | } 30 | 31 | render () { 32 | console.log(this.state.loggedInUsers); 33 | return ( 34 |
    35 | 36 |
    37 | ); 38 | } 39 | } 40 | 41 | export default UsersList; 42 | -------------------------------------------------------------------------------- /app/src/main.js: -------------------------------------------------------------------------------- 1 | // main sass file 2 | require("../static/stylesheets/sass/main.scss"); 3 | 4 | // core modules 5 | import $ from 'jquery'; 6 | import React from 'react'; 7 | import { render } from 'react-dom'; 8 | import { Router, Route, IndexRoute, browserHistory } from 'react-router'; 9 | 10 | // components 11 | import ChatterboxApp from './components/main/chatterbox_app.jsx'; 12 | import Footer from './components/main/footer.jsx'; 13 | import LoginPage from './components/auth/login_page.jsx'; 14 | import SignUpPage from './components/auth/sign_up_page.jsx'; 15 | import UserHomePage from './components/user/user_home_page.jsx'; 16 | import UsersSearchResultsPage from './components/user/search/users_search_results_page.jsx'; 17 | 18 | $(document).ready(function () { 19 | const routes = ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | ); 27 | 28 | render(, 29 | document.getElementById('content')); 30 | render(