├── src ├── client │ ├── components │ │ ├── MessageStatus │ │ │ ├── index.js │ │ │ └── style.css │ │ ├── NewConversation │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── TypingIndicator │ │ │ ├── index.js │ │ │ └── style.css │ │ ├── ViewConversation │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── RecipientsListItem │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── Conversation │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── Conversations │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── ConversationsListItem │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── ConversationsList │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── ConversationHeader │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── ConversationsListHeader │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── Message │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── ConversationThread │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── MessageInput │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── MessageRecipients │ │ │ ├── style.css │ │ │ └── index.js │ │ └── RecipientsList │ │ │ ├── style.css │ │ │ └── index.js │ ├── containers │ │ ├── RecipientsListContainer │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── ConversationThreadContainer │ │ │ ├── style.css │ │ │ └── index.js │ │ ├── ConversationsListContainer │ │ │ ├── style.css │ │ │ └── index.js │ │ └── App │ │ │ ├── index.js │ │ │ └── style.css │ ├── middleware │ │ ├── index.js │ │ └── logger.js │ ├── actions │ │ ├── profile.js │ │ ├── recipientsList.js │ │ ├── newConversation.js │ │ ├── selectedConversation.js │ │ ├── users.js │ │ ├── conversations.js │ │ └── draft.js │ ├── constants │ │ └── filters.js │ ├── reducers │ │ ├── newConversation.js │ │ ├── recipientsList.js │ │ ├── selectedConversation.js │ │ ├── profile.js │ │ ├── users.js │ │ ├── index.js │ │ ├── draft.js │ │ └── conversations.js │ ├── index.js │ ├── store │ │ └── index.js │ └── index.html ├── utils.js └── server │ ├── models │ └── user.js │ ├── views │ ├── login.ejs │ └── signup.ejs │ ├── routes.js │ ├── messaging.js │ └── auth.js ├── .gitignore ├── .babelrc ├── server.js ├── config ├── database.js ├── auth.js └── passport.js ├── TODO.md ├── webpack.config.js ├── package.json └── README.md /src/client/components/MessageStatus/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/MessageStatus/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/NewConversation/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/TypingIndicator/index.js: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/TypingIndicator/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/components/ViewConversation/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/client/containers/RecipientsListContainer/style.css: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_STORE 2 | node_modules 3 | static 4 | .module-cache 5 | *.log* 6 | 7 | -------------------------------------------------------------------------------- /src/client/middleware/index.js: -------------------------------------------------------------------------------- 1 | 2 | import logger from './logger' 3 | 4 | export { 5 | logger 6 | } -------------------------------------------------------------------------------- /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-0", "react"], 3 | "plugins": ["transform-runtime"] 4 | } 5 | -------------------------------------------------------------------------------- /src/client/components/RecipientsListItem/style.css: -------------------------------------------------------------------------------- 1 | .recipientsListItem { 2 | background-color: #FFF; 3 | height: 2.5em; 4 | } 5 | -------------------------------------------------------------------------------- /src/client/middleware/logger.js: -------------------------------------------------------------------------------- 1 | 2 | export default store => next => action => { 3 | console.log(action) 4 | return next(action) 5 | } -------------------------------------------------------------------------------- /src/client/actions/profile.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const setUsername = createAction('set username') 5 | -------------------------------------------------------------------------------- /src/client/actions/recipientsList.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const toggle = createAction('toggle recipients list') 5 | -------------------------------------------------------------------------------- /src/client/actions/newConversation.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const newConversation = createAction('new conversation') 5 | -------------------------------------------------------------------------------- /src/client/actions/selectedConversation.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const viewConversation = createAction('select conversation') 5 | -------------------------------------------------------------------------------- /src/client/constants/filters.js: -------------------------------------------------------------------------------- 1 | 2 | export const SHOW_ALL = 'show_all' 3 | export const SHOW_COMPLETED = 'show_completed' 4 | export const SHOW_ACTIVE = 'show_active' 5 | -------------------------------------------------------------------------------- /src/client/components/Conversation/style.css: -------------------------------------------------------------------------------- 1 | .conversation { 2 | position: absolute; 3 | top: 0; 4 | left: 340px; 5 | right: 0; 6 | bottom: 0; 7 | overflow: hidden; 8 | } 9 | -------------------------------------------------------------------------------- /server.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | var authServer = require('./src/server/auth'); 4 | var messagingServer = require('./src/server/messaging'); 5 | 6 | authServer.start(); 7 | messagingServer.start(); 8 | -------------------------------------------------------------------------------- /src/client/actions/users.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const addUser = createAction('add user') 5 | export const updateUserList = createAction('update user list') 6 | -------------------------------------------------------------------------------- /config/database.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: (process.env.NODE_ENV === 'test' ? 'iMsgTest' : 'iMsg'), 3 | host: 'localhost:27017', 4 | user: process.env.DATABASE_USER, 5 | password: process.env.DATABASE_PASSWORD 6 | }; 7 | -------------------------------------------------------------------------------- /src/client/components/Conversations/style.css: -------------------------------------------------------------------------------- 1 | .conversations { 2 | width: 340px; 3 | position: fixed; 4 | top: 0; 5 | left: 0; 6 | bottom: 0; 7 | border-right: 1px solid #ddd; 8 | box-sizing: border-box; 9 | overflow: hidden; 10 | } 11 | -------------------------------------------------------------------------------- /src/client/reducers/newConversation.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = false 5 | 6 | export default handleActions({ 7 | 'new conversation' (state, action) { 8 | return action.payload 9 | } 10 | }, initialState) 11 | -------------------------------------------------------------------------------- /src/client/reducers/recipientsList.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = false 5 | 6 | export default handleActions({ 7 | 'toggle recipients list' (state, action) { 8 | return !state 9 | } 10 | }, initialState) 11 | -------------------------------------------------------------------------------- /src/client/reducers/selectedConversation.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = 0 5 | 6 | export default handleActions({ 7 | 'select conversation' (state, action) { 8 | return action.payload 9 | } 10 | }, initialState) 11 | -------------------------------------------------------------------------------- /src/client/containers/ConversationThreadContainer/style.css: -------------------------------------------------------------------------------- 1 | .threadView { 2 | position: absolute; 3 | margin: 0; 4 | padding: 0; 5 | top: 65px; 6 | bottom: 0; 7 | right: 0; 8 | left: 0; 9 | background: #FFF; 10 | overflow-x: hidden; 11 | } 12 | -------------------------------------------------------------------------------- /src/client/containers/ConversationsListContainer/style.css: -------------------------------------------------------------------------------- 1 | .listView { 2 | position: absolute; 3 | top: 65px; 4 | left: 0; 5 | bottom: 0; 6 | background-color: #FFF; 7 | padding: 0; 8 | width: 339px; 9 | margin: 0; 10 | overflow-x: hidden; 11 | } 12 | -------------------------------------------------------------------------------- /src/client/reducers/profile.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = { 5 | username: '' 6 | } 7 | 8 | export default handleActions({ 9 | 'set username' (state, action) { 10 | return { 11 | username: action.payload 12 | } 13 | } 14 | }, initialState) 15 | -------------------------------------------------------------------------------- /config/auth.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | 'cookieSecret': 'ch4t5evar', 3 | 'facebookAuth' : { 4 | 'clientID' : '1557410067920133', // your App ID 5 | 'clientSecret': '5e1d09351c1c70c36e6576be644cdd86', // your App Secret 6 | 'callbackURL' : 'http://localhost:8001/auth/facebook/callback' 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/utils.js: -------------------------------------------------------------------------------- 1 | var utils = {}; 2 | 3 | utils.getUserIdFromSession = function (sessionString) { 4 | if (!sessionString) { 5 | return 6 | } 7 | 8 | var sessionBody = new Buffer(sessionString, 'base64').toString('utf8'); 9 | var jsonBody = JSON.parse(sessionBody); 10 | return jsonBody.passport.user; 11 | }; 12 | 13 | module.exports = utils; 14 | -------------------------------------------------------------------------------- /src/client/reducers/users.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = [] 5 | 6 | export default handleActions({ 7 | 'add user' (state, action) { 8 | return [ 9 | action.payload, 10 | ...state 11 | ] 12 | }, 13 | 14 | 'update user list' (state, action) { 15 | return action.payload 16 | } 17 | }, initialState) 18 | -------------------------------------------------------------------------------- /src/client/components/ConversationsListItem/style.css: -------------------------------------------------------------------------------- 1 | .listItem { 2 | display: block; 3 | overflow: hidden; 4 | } 5 | 6 | a { 7 | text-decoration: none; 8 | } 9 | 10 | h3 { 11 | color: #111; 12 | margin-bottom: 0; 13 | } 14 | 15 | .listDescription { 16 | color: #333; 17 | text-decoration: none; 18 | display: inline-block; 19 | text-overflow:ellipsis; 20 | overflow: hidden; 21 | white-space: nowrap; 22 | } 23 | -------------------------------------------------------------------------------- /src/client/components/ConversationsList/style.css: -------------------------------------------------------------------------------- 1 | ul { 2 | margin: 0; 3 | padding: 0; 4 | list-style-type: none; 5 | 6 | } 7 | 8 | .conversationsList { 9 | position: absolute; 10 | top: 65px; 11 | left: 0; 12 | bottom: 0; 13 | right: 0; 14 | overflow-y: auto; 15 | } 16 | 17 | li { 18 | border-bottom: 1px solid #ccc; 19 | height: 5em; 20 | padding: 0 1em; 21 | } 22 | 23 | .selectedListItem { 24 | background-color: #D9D9D9; 25 | } 26 | -------------------------------------------------------------------------------- /src/client/components/ConversationHeader/style.css: -------------------------------------------------------------------------------- 1 | .header { 2 | box-sizing: border-box; 3 | position: relative; 4 | z-index: 10; 5 | height: 65px; 6 | background-color: rgba(246,246,246, 0.97); 7 | border-bottom: 1px solid rgb(207,207,207); 8 | } 9 | 10 | .title { 11 | color: #111; 12 | line-height: 65px; 13 | text-align: center; 14 | font-size: 1.3em; 15 | } 16 | 17 | .titleLabel { 18 | display: inline-block; 19 | font-weight: bold; 20 | text-align: center; 21 | } 22 | -------------------------------------------------------------------------------- /src/client/actions/conversations.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | 4 | export const addConversation = createAction('add conversation') 5 | export const deleteConversation = createAction('delete converstaion') 6 | export const addMessage = createAction('add message') 7 | export const receiveMessage = createAction('receive message') 8 | export const readMessage = createAction('read message') 9 | export const startTyping = createAction('start typing') 10 | export const updateConversationList = createAction('update conversation list') 11 | -------------------------------------------------------------------------------- /src/client/components/Conversation/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import ConversationThreadContainer from '../../containers/ConversationThreadContainer' 4 | import style from './style.css' 5 | 6 | class Conversation extends Component { 7 | render() { 8 | const { conversation } = this.props 9 | console.log('~~~ a'); 10 | return ( 11 |
12 | 13 |
14 | ) 15 | } 16 | } 17 | 18 | export default Conversation 19 | -------------------------------------------------------------------------------- /src/client/components/Conversations/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import ConversationsListHeader from '../ConversationsListHeader' 4 | import ConversationsListContainer from '../../containers/ConversationsListContainer' 5 | import style from './style.css' 6 | 7 | class Conversations extends Component { 8 | render() { 9 | const { conversations } = this.props 10 | return ( 11 |
12 | 13 | 14 |
15 | ) 16 | } 17 | } 18 | 19 | export default Conversations 20 | -------------------------------------------------------------------------------- /src/client/reducers/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { routeReducer as routing } from 'react-router-redux' 3 | import { combineReducers } from 'redux' 4 | import conversations from './conversations' 5 | import users from './users' 6 | import recipientsList from './recipientsList' 7 | import draft from './draft' 8 | import profile from './profile' 9 | import selectedConversation from './selectedConversation' 10 | import newConversation from './newConversation' 11 | 12 | export default combineReducers({ 13 | routing, 14 | conversations, 15 | users, 16 | recipientsList, 17 | draft, 18 | profile, 19 | selectedConversation, 20 | newConversation 21 | }) 22 | -------------------------------------------------------------------------------- /src/client/components/ConversationsListHeader/style.css: -------------------------------------------------------------------------------- 1 | .header { 2 | box-sizing: border-box; 3 | position: relative; 4 | height: 65px; 5 | background-color: rgb(246,246,246); 6 | border-bottom: 1px solid rgb(207,207,207); 7 | } 8 | 9 | .title { 10 | color: #111; 11 | line-height: 65px; 12 | text-align: center; 13 | font-size: 1.3em; 14 | } 15 | 16 | .titleLabel { 17 | display: inline-block; 18 | font-weight: bold; 19 | text-align: center; 20 | } 21 | 22 | .titleRightAction { 23 | display: inline-block; 24 | position: absolute; 25 | color: rgb(20,111,246); 26 | text-decoration: none; 27 | right: 0; 28 | width: 65px; 29 | line-height: 65px; 30 | height: 65px; 31 | font-size: 1.7em; 32 | } 33 | -------------------------------------------------------------------------------- /src/server/models/user.js: -------------------------------------------------------------------------------- 1 | var mongoose = require('mongoose'); 2 | var bcrypt = require('bcrypt-nodejs'); 3 | 4 | // define the schema for our user model 5 | var userSchema = mongoose.Schema({ 6 | local : { 7 | username : String, 8 | password : String 9 | } 10 | }); 11 | 12 | // generating a hash 13 | userSchema.methods.generateHash = function(password) { 14 | return bcrypt.hashSync(password, bcrypt.genSaltSync(8), null); 15 | }; 16 | 17 | // checking if password is valid 18 | userSchema.methods.validPassword = function(password) { 19 | return bcrypt.compareSync(password, this.local.password); 20 | }; 21 | 22 | // create the model for users and expose it to our app 23 | module.exports = mongoose.model('User', userSchema); 24 | -------------------------------------------------------------------------------- /src/client/components/Message/style.css: -------------------------------------------------------------------------------- 1 | .messageWrapper { 2 | display: block; 3 | width: 100%; 4 | height: initial; 5 | } 6 | 7 | .message { 8 | display: inline-block; 9 | position: relative; 10 | float: left; 11 | clear: both; 12 | background-color: #E5E5EA; 13 | color: #111; 14 | border-radius: 13px; 15 | height: initial; 16 | padding: .3em .75em; 17 | margin-top: 3px; 18 | border: none; 19 | } 20 | 21 | .messageRight { 22 | display: inline-block; 23 | position: relative; 24 | float: right; 25 | clear: both; 26 | background-color: rgb(20,111,246); 27 | color: #FFF; 28 | border-radius: 13px; 29 | height: initial; 30 | padding: .3em .75em; 31 | margin-top: 3px; 32 | border: none; 33 | } 34 | -------------------------------------------------------------------------------- /src/client/components/ConversationHeader/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import style from './style.css' 5 | 6 | class ConversationHeader extends Component { 7 | render() { 8 | const { newConversation, titleLabel } = this.props 9 | return ( 10 |
11 |
12 | {newConversation ? 'New message' : titleLabel} 13 |
14 |
15 | ) 16 | } 17 | } 18 | 19 | function mapStateToProps(state) { 20 | var conversationsState = state.conversations 21 | return { 22 | newConversation: state.newConversation 23 | } 24 | } 25 | export default connect(mapStateToProps)(ConversationHeader) 26 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | # TODO 2 | 3 | * Deploy to AWS via Elastic Beanstalk and a Mongo instance 4 | 5 | ## Tooling 6 | * Test suite 7 | * Linting 8 | * Continuous Integration 9 | 10 | ## Client side 11 | * Mobile friendly/responsive web application 12 | * Typing indicator 13 | * Message status (delivered / read) 14 | * Time stamp indicators 15 | * Consecutive messages within minute are tightly bunched. 16 | * Groups within hour have timestamp heading 17 | * Last message in bunch has tick 18 | * Support Unicode 8.0 to allow emojis 19 | * Handle delivered acknowledgement 20 | * Handle read receipt 21 | * Direct manipulation pan to reveal timestamps 22 | 23 | ## Server side 24 | * paginated history via minimal rest api 25 | * Persist to data store and add caching (e.g. Redis integration to Deepstream.io) instead of default in-memory storage 26 | -------------------------------------------------------------------------------- /src/client/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { Router, Route, browserHistory, IndexRoute } from 'react-router' 3 | import { Provider } from 'react-redux' 4 | import ReactDOM from 'react-dom' 5 | import React from 'react' 6 | 7 | import App from './containers/App' 8 | import NewConversation from './components/NewConversation' 9 | import ViewConversation from './components/ViewConversation' 10 | import configure from './store' 11 | 12 | var store = configure() 13 | ReactDOM.render( 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | , 22 | document.getElementById('root') 23 | ) 24 | -------------------------------------------------------------------------------- /src/client/components/ConversationThread/style.css: -------------------------------------------------------------------------------- 1 | .threadContainer { 2 | position: absolute; 3 | top: 0; 4 | bottom: 55px; 5 | left: 0; 6 | right: 0; 7 | border-bottom: 1px solid rgb(207,207,207); 8 | background-color: #FFF; 9 | overflow-y: auto; 10 | -webkit-overflow-scrolling: touch; 11 | padding: 10px; 12 | display: flex; 13 | flex-direction: column; 14 | } 15 | 16 | .threadList { 17 | position: absolute; 18 | top: 0; 19 | bottom: 0; 20 | left: 0; 21 | right: 0; 22 | } 23 | 24 | .threadContainer ul li { 25 | display: block; 26 | border: none; 27 | overflow: hidden; 28 | margin: 0; 29 | padding: 0; 30 | } 31 | 32 | .listPadder { 33 | flex-grow: 1; 34 | position: relative; 35 | top: 0; 36 | bottom: 0; 37 | left: 0; 38 | right: 0; 39 | min-height: 120px; 40 | } 41 | -------------------------------------------------------------------------------- /src/client/store/index.js: -------------------------------------------------------------------------------- 1 | 2 | import { createStore, applyMiddleware } from 'redux' 3 | import { syncHistory } from 'react-router-redux' 4 | import { browserHistory } from 'react-router' 5 | import thunk from 'redux-thunk' 6 | 7 | import { logger } from '../middleware' 8 | import rootReducer from '../reducers' 9 | 10 | export default function configure(initialState) { 11 | const create = window.devToolsExtension 12 | ? window.devToolsExtension()(createStore) 13 | : createStore 14 | 15 | const createStoreWithMiddleware = applyMiddleware( 16 | logger, 17 | syncHistory(browserHistory), 18 | thunk 19 | )(create) 20 | 21 | const store = createStoreWithMiddleware(rootReducer, initialState) 22 | 23 | if (module.hot) { 24 | module.hot.accept('../reducers', () => { 25 | const nextReducer = require('../reducers') 26 | store.replaceReducer(nextReducer) 27 | }) 28 | } 29 | 30 | return store 31 | } 32 | -------------------------------------------------------------------------------- /src/client/reducers/draft.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = { 5 | recipients: [], 6 | availableRecipients: [], 7 | body: '' 8 | } 9 | 10 | export default handleActions({ 11 | 'new draft' (state, action) { 12 | return { 13 | recipients: [], 14 | body: '' 15 | } 16 | }, 17 | 18 | 'add recipient' (state, action) { 19 | var u = {} 20 | var recipients = [] 21 | for (let recipient of [...state.recipients, 'users/'+action.payload]) { 22 | if (!u.hasOwnProperty(recipient) ) { 23 | u[recipient] = recipient 24 | recipients.push(recipient) 25 | } 26 | } 27 | return { 28 | recipients: recipients, 29 | body: state.body 30 | } 31 | }, 32 | 33 | 'remove recipient' (state, action) { 34 | }, 35 | 36 | 'add message body' (state, action) { 37 | return { 38 | recipients: [...state.recipients], 39 | body: action.payload 40 | } 41 | } 42 | }, initialState) 43 | -------------------------------------------------------------------------------- /src/client/components/NewConversation/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | import Conversations from '../../components/Conversations' 7 | import Conversation from '../../components/Conversation' 8 | import * as ConversationActions from '../../actions/conversations' 9 | import * as NewConversationActions from '../../actions/newConversation' 10 | import * as DraftActions from '../../actions/draft' 11 | import App from '../../containers/App' 12 | import style from './style.css' 13 | 14 | class NewConversation extends Component { 15 | componentWillMount() { 16 | const { dispatch } = this.props 17 | dispatch(NewConversationActions.newConversation(true)) 18 | dispatch(DraftActions.clearDraft()) 19 | } 20 | 21 | render() { 22 | return ( 23 |
24 | 25 | 26 |
27 | ) 28 | } 29 | } 30 | 31 | export default connect()(NewConversation) 32 | -------------------------------------------------------------------------------- /src/client/components/ConversationsListHeader/index.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react' 2 | import { connect } from 'react-redux' 3 | import { browserHistory } from 'react-router' 4 | 5 | import style from './style.css' 6 | import * as NewConversationActions from '../../actions/newConversation' 7 | import * as DraftActions from '../../actions/draft' 8 | 9 | class ConversationsListHeader extends Component { 10 | onAddConversationClick(e) { 11 | e.preventDefault() 12 | const { dispatch } = this.props 13 | browserHistory.push('/') 14 | } 15 | 16 | render() { 17 | const users = this.props.users 18 | return ( 19 |
20 |
21 | Messages 22 | + 23 |
24 |
25 | ) 26 | } 27 | } 28 | 29 | export default connect()(ConversationsListHeader) 30 | -------------------------------------------------------------------------------- /src/client/components/ConversationsList/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import ConversationsListItem from '../ConversationsListItem' 4 | import style from './style.css' 5 | 6 | class ConversationsList extends Component { 7 | render() { 8 | const { newConversation, selectedConversation, conversations, conversationsById, onConversationClick } = this.props 9 | var items = [] 10 | if (newConversation) { 11 | items.push(
  • ) 12 | } 13 | for (let index in conversations) { 14 | var conversationId = conversations[index] 15 | items.push(
  • ) 16 | } 17 | return ( 18 | 19 | ) 20 | } 21 | } 22 | 23 | export default ConversationsList 24 | -------------------------------------------------------------------------------- /src/client/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | iMsg 7 | 8 | 9 |
    10 | 11 | 25 | 26 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/client/components/MessageInput/style.css: -------------------------------------------------------------------------------- 1 | .messageInput { 2 | position: absolute; 3 | display: flex; 4 | height: 55px; 5 | bottom: 0; 6 | left: 0; 7 | right: 0; 8 | background-color: #F5F5F3; 9 | } 10 | 11 | .messageInput textarea { 12 | -webkit-box-flex: 1; 13 | flex-grow: 1; 14 | font-size: 14px; 15 | text-indent: 8px; 16 | resize: none; 17 | color: rgba(0, 0, 0, 0.870588); 18 | line-height: 30px; 19 | overflow-x: hidden; 20 | overflow-y: hidden; 21 | background-color: rgb(250, 251, 252); 22 | border-image-source: initial; 23 | border-image-slice: initial; 24 | border-image-width: initial; 25 | border-image-outset: initial; 26 | border-image-repeat: initial; 27 | outline: none; 28 | margin: 10px; 29 | border: 1px solid rgb(228, 233, 236); 30 | border-radius: 4px; 31 | } 32 | 33 | .button { 34 | display: inline-block; 35 | color: rgb(20,111,246); 36 | text-decoration: none; 37 | width: 55px; 38 | height: 55px; 39 | line-height: 55px; 40 | font-size: 1.3em; 41 | font-weight: 700; 42 | } 43 | -------------------------------------------------------------------------------- /src/client/components/Message/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | 5 | import App from '../../containers/App' 6 | import style from './style.css' 7 | 8 | class Message extends Component { 9 | componentDidMount() { 10 | const { message } = this.props 11 | console.log('<<< MESSAGE ID: ', message); 12 | this.record = App.ds.record.getRecord(message); 13 | this.record.whenReady( function() { 14 | setTimeout(function () { 15 | this.setState(this.record.get() ); 16 | }.bind(this), 250); 17 | }.bind( this )); 18 | } 19 | 20 | render() { 21 | const { profile } = this.props 22 | return ( 23 |
  • 24 |
    25 | { this.state ? this.state.body : '' } 26 |
    27 |
  • 28 | ) 29 | } 30 | } 31 | 32 | function mapStateToProps(state) { 33 | return { 34 | profile: state.profile 35 | } 36 | } 37 | 38 | export default connect(mapStateToProps)(Message) 39 | -------------------------------------------------------------------------------- /src/client/components/RecipientsListItem/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | 5 | import style from './style.css' 6 | import deepstream from 'deepstream.io-client-js' 7 | import App from '../../containers/App' 8 | import * as DraftActions from '../../actions/draft' 9 | import * as RecipientsListActions from '../../actions/recipientsList' 10 | 11 | class RecipientsListItem extends Component { 12 | onClick(e) { 13 | const { dispatch } = this.props 14 | e.preventDefault() 15 | dispatch(DraftActions.addRecipient(this.state.username)) 16 | dispatch(RecipientsListActions.toggle()) 17 | } 18 | 19 | componentDidMount() { 20 | const { user } = this.props 21 | this.record = App.ds.record.getRecord(user); 22 | this.record.whenReady( function() { 23 | this.setState(this.record.get() ); 24 | }.bind( this )); 25 | } 26 | 27 | render() { 28 | return ( 29 |
  • 30 |

    {this.state ? this.state.username : 'wut'}

    31 |
  • 32 | ) 33 | } 34 | } 35 | 36 | export default connect()(RecipientsListItem) 37 | -------------------------------------------------------------------------------- /src/client/containers/ConversationsListContainer/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | import ConversationsList from '../../components/ConversationsList' 7 | import style from './style.css' 8 | 9 | class ConversationsListContainer extends Component { 10 | render() { 11 | const { newConversation, selectedConversation, conversations, conversationsById } = this.props 12 | return ( 13 | 18 | ) 19 | } 20 | } 21 | 22 | function mapStateToProps(state) { 23 | var conversationsState = state.conversations 24 | return { 25 | newConversation: state.newConversation, 26 | selectedConversation: state.selectedConversation, 27 | conversations: conversationsState.conversations, 28 | conversationsById: conversationsState.conversationsById 29 | } 30 | } 31 | 32 | export default connect( 33 | mapStateToProps 34 | )(ConversationsListContainer) 35 | -------------------------------------------------------------------------------- /src/client/components/MessageRecipients/style.css: -------------------------------------------------------------------------------- 1 | .messageRecipients { 2 | position: relative; 3 | z-index: 10; 4 | display: flex; 5 | height: 55px; 6 | top: 0; 7 | left: 0; 8 | right: 0; 9 | background-color: rgba(246,246,246, 0.95); 10 | border-bottom: 1px solid rgb(207,207,207); 11 | } 12 | 13 | .toLabel { 14 | display: inline-block; 15 | height: 55px; 16 | line-height: 55px; 17 | width: 30px; 18 | padding-left: 20px; 19 | } 20 | 21 | textarea { 22 | -webkit-box-flex: 1; 23 | flex-grow: 1; 24 | font-size: 14px; 25 | text-indent: 8px; 26 | resize: none; 27 | color: rgba(0, 0, 0, 0.870588); 28 | line-height: 30px; 29 | overflow-x: hidden; 30 | overflow-y: hidden; 31 | background-color: transparent; 32 | outline: none; 33 | margin: 10px; 34 | border: none; 35 | border-radius: 4px; 36 | } 37 | 38 | .titleRightAction { 39 | display: inline-block; 40 | position: absolute; 41 | color: rgb(20,111,246); 42 | text-decoration: none; 43 | right: 0; 44 | line-height: 20px; 45 | width: 25px; 46 | height: 25px; 47 | font-size: 1.7em; 48 | text-align: center; 49 | border: 1px solid rgb(20,111,246); 50 | border-radius: 25px; 51 | margin: 15px; 52 | } 53 | -------------------------------------------------------------------------------- /src/client/containers/RecipientsListContainer/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | import RecipientsList from '../../components/RecipientsList' 7 | import * as UserActions from '../../actions/users' 8 | import style from './style.css' 9 | 10 | class RecipientsListContainer extends Component { 11 | render() { 12 | const { users, selectedRecipients, showRecipientsList } = this.props 13 | var availableRecipients = users.filter(function (userRecordName) { 14 | if (userRecordName && selectedRecipients.indexOf(userRecordName) === -1) { 15 | return userRecordName 16 | } 17 | }) 18 | return ( 19 | 20 | ) 21 | } 22 | } 23 | 24 | function mapStateToProps(state) { 25 | return { 26 | users: state.users, 27 | selectedRecipients: state.draft.recipients, 28 | showRecipientsList: state.recipientsList 29 | } 30 | } 31 | 32 | function mapDispatchToProps(dispatch) { 33 | return { 34 | actions: bindActionCreators(UserActions, dispatch) 35 | } 36 | } 37 | 38 | 39 | export default connect( 40 | mapStateToProps, 41 | mapDispatchToProps 42 | )(RecipientsListContainer) 43 | -------------------------------------------------------------------------------- /src/client/components/RecipientsList/style.css: -------------------------------------------------------------------------------- 1 | .animate { 2 | -webkit-transition: top 0.3s ease-in; 3 | -moz-transition: top 0.3s ease-in; 4 | -ms-transition: top 0.3s ease-in; 5 | -o-transition: top 0.3s ease-in; 6 | transition: top 0.3s ease-in; 7 | } 8 | .recipientsList { 9 | position: absolute; 10 | z-index: 10; 11 | display: block; 12 | top: 100%; 13 | left: 0; 14 | right: 0; 15 | bottom: 0; 16 | background-color: #FFF; 17 | } 18 | 19 | .recipientsListVisible { 20 | position: absolute; 21 | z-index: 10; 22 | display: block; 23 | top: 0px; 24 | left: 0; 25 | right: 0; 26 | bottom: 0; 27 | background-color: #FFF; 28 | } 29 | 30 | .header { 31 | box-sizing: border-box; 32 | position: relative; 33 | z-index: 10; 34 | height: 65px; 35 | background-color: rgba(246,246,246, 0.95); 36 | border-bottom: 1px solid rgb(207,207,207); 37 | } 38 | 39 | .recipientsList .title, 40 | .recipientsListVisible .title{ 41 | color: #111; 42 | line-height: 65px; 43 | text-align: center; 44 | font-size: 1.3em; 45 | } 46 | 47 | .titleLabel { 48 | display: inline-block; 49 | font-weight: bold; 50 | text-align: center; 51 | } 52 | 53 | .titleRightAction { 54 | display: inline-block; 55 | position: absolute; 56 | color: rgb(20,111,246); 57 | text-decoration: none; 58 | right: 10px; 59 | width: 65px; 60 | line-height: 65px; 61 | height: 65px; 62 | font-size: 1em; 63 | } 64 | -------------------------------------------------------------------------------- /src/server/views/login.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 10 | 11 | 12 |
    13 | 14 |
    15 | 16 |

    Login

    17 | 18 | 19 | <% if (message.length > 0) { %> 20 |
    <%= message %>
    21 | <% } %> 22 | 23 | 24 |
    25 |
    26 | 27 | 28 |
    29 |
    30 | 31 | 32 |
    33 | 34 | 35 |
    36 | 37 |
    38 | 39 |

    Need an account? Signup

    40 |

    Or go home.

    41 | 42 |
    43 | 44 |
    45 | 46 | 47 | -------------------------------------------------------------------------------- /src/server/views/signup.ejs: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Node Authentication 5 | 6 | 7 | 10 | 11 | 12 |
    13 | 14 |
    15 | 16 |

    Signup

    17 | 18 | 19 | <% if (message.length > 0) { %> 20 |
    <%= message %>
    21 | <% } %> 22 | 23 | 24 |
    25 |
    26 | 27 | 28 |
    29 |
    30 | 31 | 32 |
    33 | 34 | 35 |
    36 | 37 |
    38 | 39 |

    Already have an account? Login

    40 |

    Or go home.

    41 | 42 |
    43 | 44 |
    45 | 46 | 47 | -------------------------------------------------------------------------------- /src/client/components/ConversationsListItem/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import { Link } from 'react-router' 6 | import App from '../../containers/App' 7 | 8 | import style from './style.css' 9 | 10 | class ConversationListItem extends Component { 11 | componentWillReceiveProps(nextProps) { 12 | const { conversation } = nextProps 13 | if (conversation) { 14 | this.conversationRecord = App.ds.record.getRecord(conversation.id) 15 | this.conversationRecord.whenReady(() => { 16 | var lastMessage = this.conversationRecord.get('lastMessage') 17 | this.setState({ lastMessage: lastMessage }) 18 | 19 | this.conversationRecord.subscribe('lastMessage', (lastMessage) => { 20 | this.setState({ lastMessage: lastMessage }) 21 | }) 22 | }) 23 | } 24 | } 25 | 26 | componentWillUnmount() { 27 | if (this.conversationRecord) { 28 | this.conversationRecord.unsubscribe('lastMessage') 29 | } 30 | } 31 | 32 | render() { 33 | const { conversation } = this.props 34 | return ( 35 | 36 |

    {conversation ? conversation.title : ''}

    37 | {this.state ? this.state.lastMessage : ''} 38 | 39 | ) 40 | } 41 | } 42 | 43 | export default connect()(ConversationListItem) 44 | -------------------------------------------------------------------------------- /webpack.config.js: -------------------------------------------------------------------------------- 1 | var rucksack = require('rucksack-css') 2 | var webpack = require('webpack') 3 | var path = require('path') 4 | 5 | module.exports = { 6 | context: path.join(__dirname, './src/client'), 7 | entry: { 8 | jsx: './index.js', 9 | html: './index.html', 10 | vendor: ['react', 'deepstream.io-client-js'] 11 | }, 12 | output: { 13 | path: path.join(__dirname, './static'), 14 | filename: 'bundle.js', 15 | }, 16 | module: { 17 | loaders: [ 18 | { 19 | test: /\.html$/, 20 | loader: 'file?name=[name].[ext]' 21 | }, 22 | { 23 | test: /\.css$/, 24 | loaders: [ 25 | 'style-loader', 26 | 'css-loader?modules&sourceMap&importLoaders=1&localIdentName=[name]__[local]___[hash:base64:5]', 27 | 'postcss-loader' 28 | ] 29 | }, 30 | { 31 | test: /\.(js|jsx)$/, 32 | exclude: /node_modules/, 33 | loaders: [ 34 | 'react-hot', 35 | 'babel-loader' 36 | ] 37 | }, 38 | ], 39 | }, 40 | resolve: { 41 | extensions: ['', '.js', '.jsx'] 42 | }, 43 | postcss: [ 44 | rucksack({ 45 | autoprefixer: true 46 | }) 47 | ], 48 | plugins: [ 49 | new webpack.optimize.CommonsChunkPlugin('vendor', 'vendor.bundle.js'), 50 | new webpack.DefinePlugin({ 51 | 'process.env': { NODE_ENV: JSON.stringify(process.env.NODE_ENV || 'development') } 52 | }) 53 | ], 54 | devServer: { 55 | contentBase: './src/client', 56 | hot: true 57 | }, 58 | node: { 59 | fs: 'empty', 60 | net: 'empty', 61 | tls: 'empty' 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/client/components/RecipientsList/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | 5 | import RecipientsListItem from '../RecipientsListItem' 6 | import * as RecipientsListActions from '../../actions/recipientsList' 7 | import style from './style.css' 8 | 9 | class RecipientsList extends Component { 10 | onCancelRecipientsList(e) { 11 | e.preventDefault() 12 | const { dispatch } = this.props 13 | dispatch(RecipientsListActions.toggle()) 14 | } 15 | 16 | render() { 17 | const { users, profile, visible } = this.props 18 | var items = [] 19 | var filtered = users.filter(function (user) { 20 | if (user == 'users/' + profile.username) { 21 | return false 22 | } 23 | return true 24 | }) 25 | console.log(filtered, profile.username) 26 | for (let index in filtered) { 27 | items.push() 28 | } 29 | return ( 30 |
    31 |
    32 |
    33 | Send To 34 | Cancel 35 |
    36 |
    37 |
      {items}
    38 |
    39 | ) 40 | } 41 | } 42 | 43 | function mapStateToProps(state) { 44 | return { 45 | profile: state.profile 46 | } 47 | } 48 | 49 | export default connect(mapStateToProps)(RecipientsList) 50 | -------------------------------------------------------------------------------- /src/client/components/MessageRecipients/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | import * as RecipientsListActions from '../../actions/recipientsList' 7 | import style from './style.css' 8 | 9 | class MessageRecipients extends Component { 10 | onClickAddRecipient(e) { 11 | e.preventDefault() 12 | const { actions } = this.props 13 | actions.toggle() 14 | } 15 | 16 | render() { 17 | const { newConversation } = this.props 18 | var addRecipientButton 19 | if (newConversation) { 20 | addRecipientButton = 21 | + 22 | } 23 | const { recipients } = this.props 24 | var recipientsPretty = recipients.map(function (recipient) { 25 | return recipient.split('/')[1] 26 | }) 27 | return ( 28 |
    29 | To: 30 | 31 | {addRecipientButton} 32 |
    33 | ) 34 | } 35 | } 36 | 37 | function mapStateToProps(state) { 38 | var draftState = state.draft 39 | return { 40 | newConversation: state.newConversation, 41 | recipients: draftState.recipients, 42 | availableRecipients: draftState.availableRecipients, 43 | body: draftState.body 44 | } 45 | } 46 | 47 | function mapDispatchToProps(dispatch) { 48 | return { 49 | actions: bindActionCreators(RecipientsListActions, dispatch) 50 | } 51 | } 52 | 53 | export default connect( 54 | mapStateToProps, 55 | mapDispatchToProps 56 | )(MessageRecipients) 57 | -------------------------------------------------------------------------------- /src/client/reducers/conversations.js: -------------------------------------------------------------------------------- 1 | 2 | import { handleActions } from 'redux-actions' 3 | 4 | const initialState = { 5 | conversations: [], 6 | conversationsById: {} 7 | } 8 | 9 | export default handleActions({ 10 | 'view conversation' (state, action) { 11 | return Object.assign({}, state, { 12 | selectedConversation: action.payload 13 | }) 14 | }, 15 | 16 | 'add conversation' (state, action) { 17 | var conversation = action.payload 18 | conversation.title = action.payload.title || (conversation.participants ? conversation.participants.map(function (participant) { 19 | return participant.split('/')[1] 20 | }).join(', ') : 'New Message') 21 | var conversations = [...state.conversations] 22 | if (conversations.indexOf(conversation.id) == -1) { 23 | conversations.unshift(conversation.id) 24 | } 25 | var conversationsById = Object.assign({}, state.conversationsById) 26 | conversationsById[conversation.id] = conversation 27 | 28 | return Object.assign({}, state, { 29 | conversations: conversations, 30 | conversationsById: conversationsById 31 | }) 32 | }, 33 | 34 | 'add message' (state, action) { 35 | var conversationsById = Object.assign({}, state.conversationsById) 36 | conversationsById[action.payload.conversationId].messages = [action.payload, ...state.conversationsById[action.payload.conversationId].messages] 37 | conversationsById[action.payload.conversationId].lastMessage = action.payload.body 38 | return Object.assign({}, state, { 39 | conversationsById: conversationsById 40 | }) 41 | }, 42 | 43 | 'update conversation list' (state, action) { 44 | return Object.assign({}, state, { 45 | conversations: [...action.payload] 46 | }) 47 | } 48 | }, initialState) 49 | -------------------------------------------------------------------------------- /src/server/routes.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | 3 | module.exports = function (app, passport) { 4 | 5 | // route for chat app 6 | app.get('/', isLoggedIn, function (req, res) { 7 | res.redirect('localhost:3000/'); 8 | }); 9 | 10 | // login page 11 | app.get('/login', function (req, res) { 12 | res.render(path.join(__dirname, './views/login.ejs'), { message: req.flash('loginMessage') }); 13 | }); 14 | 15 | // process the login form 16 | app.post('/login', passport.authenticate('local-login', { 17 | successRedirect : 'http://localhost:3000/', // redirect the secure app 18 | failureRedirect : '/login', // redirect back to the signup page if there is an error 19 | failureFlash : true // allow flash messages 20 | })); 21 | 22 | // signup page 23 | app.get('/signup', function (req, res) { 24 | res.render(path.join(__dirname, './views/signup.ejs'), { message: req.flash('signupMessage') }); 25 | }); 26 | 27 | app.post('/signup', passport.authenticate('local-signup', { 28 | successRedirect : '/login', // redirect to the secure app 29 | failureRedirect : '/signup', // redirect back to the signup page if there is an error 30 | failureFlash : true // allow flash messages 31 | })); 32 | 33 | // route for logging out 34 | app.get('/logout', function(req, res) { 35 | req.logout(); 36 | res.redirect('/login'); 37 | }); 38 | }; 39 | 40 | // route middleware to make sure a user is logged in 41 | function isLoggedIn(req, res, next) { 42 | 43 | // if user is authenticated in the session, carry on 44 | if (req.isAuthenticated()) { 45 | return next(); 46 | } 47 | 48 | // if they aren't redirect the login 49 | res.redirect('/login'); 50 | } 51 | -------------------------------------------------------------------------------- /src/client/components/MessageInput/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import style from './style.css' 6 | import App from '../../containers/App' 7 | import * as DraftActions from '../../actions/draft' 8 | 9 | function getTitleFromRecipients(recipients) { 10 | return title 11 | } 12 | 13 | class MessageInput extends Component { 14 | onKeyPress(e) { 15 | if (e.which != 13 || e.shiftKey) { 16 | return 17 | } 18 | e.preventDefault() 19 | const { dispatch, draft } = this.props 20 | 21 | // If no recipients, no-op 22 | if (draft.recipients.length == 0) { 23 | console.log('Message has no recipients!') 24 | return 25 | } 26 | 27 | var messageBody = this.textarea.value 28 | if (messageBody) { 29 | dispatch(DraftActions.sendDraft(messageBody)) 30 | } 31 | this.textarea.value = '' 32 | } 33 | 34 | onSendClick(e) { 35 | e.preventDefault() 36 | const { dispatch, draft } = this.props 37 | 38 | // If no recipients, no-op 39 | if (draft.recipients.length == 0) { 40 | console.log('Message has no recipients!') 41 | return 42 | } 43 | 44 | var messageBody = this.textarea.value 45 | if (messageBody) { 46 | dispatch(DraftActions.sendDraft(messageBody)) 47 | } 48 | this.textarea.value = '' 49 | } 50 | 51 | render() { 52 | return ( 53 |
    54 | 55 | Send 56 |
    57 | ) 58 | } 59 | } 60 | 61 | function mapStateToProps(state) { 62 | var draftState = state.draft 63 | return { 64 | draft: draftState 65 | } 66 | } 67 | 68 | export default connect( 69 | mapStateToProps 70 | )(MessageInput) 71 | -------------------------------------------------------------------------------- /src/server/messaging.js: -------------------------------------------------------------------------------- 1 | //var RedisCacheConnector = require( 'deepstream.io-cache-redis' ); 2 | //var RedisMessageConnector = require( 'deepstream.io-msg-redis' ); 3 | var DeepstreamServer = require('deepstream.io'); 4 | var server = new DeepstreamServer(); 5 | var C = server.constants; 6 | var cookieParser = require('cookie-parser'); 7 | var cookieSecret = require('../../config/auth').cookieSecret; 8 | var User = require('./models/user'); 9 | var utils = require('../utils'); 10 | 11 | var authServer = { 12 | start: function () { 13 | // Optionally you can specify some settings, a full list of which 14 | // can be found here //deepstream.io/docs/deepstream.html 15 | server.set( 'host', 'localhost' ); 16 | server.set( 'port', 6020 ); 17 | 18 | /* 19 | server.set( 'cache', new RedisCacheConnector({ 20 | port: 6379, 21 | host: 'localhost' 22 | })); 23 | 24 | server.set( 'messageConnector', new RedisMessageConnector({ 25 | port: 6379, 26 | host: 'localhost' 27 | })); 28 | */ 29 | 30 | server.set('permissionHandler', { 31 | isValidUser: function( connectionData, authData, callback ) { 32 | if (authData.sid) { 33 | var userId = utils.getUserIdFromSession(authData.sid); 34 | User.findById(userId, function (err, foundUser) { 35 | callback(err, userId, foundUser.local.toObject()); 36 | }); 37 | } else { 38 | callback('Invalid credentials'); 39 | } 40 | }, 41 | 42 | canPerformAction: function( username, message, callback ) { 43 | callback( null, true ); 44 | }, 45 | 46 | onClientDisconnect: function( username ){} // this one is optional 47 | }); 48 | 49 | // start the server 50 | server.start(); 51 | } 52 | } 53 | 54 | module.exports = authServer; 55 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "frontend-boilerplate", 3 | "version": "1.0.0", 4 | "private": true, 5 | "description": "A boilerplate of things that shouldn't exist", 6 | "main": "server.js", 7 | "scripts": { 8 | "test": "echo \"Error: no test specified\" && exit 1", 9 | "start": "npm run start:express & npm run start:webpack", 10 | "start:express": "node server.js", 11 | "start:webpack": "webpack-dev-server --history-api-fallback --hot --inline --progress --colors --port 3000", 12 | "build": "NODE_ENV=production webpack --progress --colors" 13 | }, 14 | "devDependencies": { 15 | "babel-core": "^6.3.26", 16 | "babel-loader": "^6.2.0", 17 | "babel-plugin-transform-runtime": "^6.3.13", 18 | "babel-preset-es2015": "^6.3.13", 19 | "babel-preset-react": "^6.3.13", 20 | "babel-preset-stage-0": "^6.3.13", 21 | "babel-runtime": "^6.3.19", 22 | "css-loader": "^0.23.1", 23 | "deepstream.io-client-js": "0.4.0", 24 | "file-loader": "^0.8.4", 25 | "js-cookie": "2.1.0", 26 | "postcss-loader": "^0.8.0", 27 | "react": "^0.14.0", 28 | "react-dom": "^0.14.0", 29 | "react-hot-loader": "^1.3.0", 30 | "react-redux": "^4.0.6", 31 | "react-router": "^2.0.0-rc5", 32 | "react-router-redux": "^2.1.0", 33 | "redux": "^3.0.2", 34 | "redux-actions": "^0.9.0", 35 | "redux-thunk": "1.0.3", 36 | "rucksack-css": "^0.8.5", 37 | "style-loader": "^0.12.4", 38 | "webpack": "^1.12.2", 39 | "webpack-dev-server": "^1.12.0", 40 | "webpack-hot-middleware": "^2.2.0" 41 | }, 42 | "dependencies": { 43 | "bcrypt-nodejs": "0.0.3", 44 | "body-parser": "1.15.0", 45 | "connect-ensure-login": "0.1.1", 46 | "connect-flash": "0.1.1", 47 | "connect-mongo": "0.8.1", 48 | "cookie-parser": "1.4.1", 49 | "cookie-session": "1.2.0", 50 | "deepstream.io": "0.8.0", 51 | "ejs": "^2.5.5", 52 | "express": "4.13.4", 53 | "express-session": "1.13.0", 54 | "mongoose": "4.4.4", 55 | "morgan": "1.7.0", 56 | "passport": "0.3.2", 57 | "passport-local": "1.0.0", 58 | "session-file-store": "0.0.24" 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # iMsg 3 | 4 | > An iMessage-like web-based messaging client built with: 5 | * React 6 | * Redux 7 | * Deepstream.io 8 | * Express + Passport.js 9 | 10 | ## TODO 11 | See [TODO.md](TODO.md) 12 | 13 | ## Component Tree 14 | ``` 15 | App 16 | |_ NewConversation 17 | |_ ViewConversation 18 | |_ Conversations 19 | |_ ConversationListHeader 20 | |_ ConversationListContainer (Container) 21 | | ConversationList 22 | |_ ConversationListItem 23 | |_ Conversation 24 | |_ ConversationHeader 25 | |_ ConversationThreadContainer (Container) 26 | |_ ConversationThread 27 | |_ HourMessageGroup? 28 | |_ MinuteMessageGroup? 29 | |_ Message 30 | |_ Avatar 31 | |_ AuthorLabel 32 | |_ MessageBody 33 | |_ MessageStatus 34 | |_ TypingIndicator 35 | |_ ConversationInput 36 | ``` 37 | 38 | ## Records 39 | * User - `users/` 40 | * Conversation - `conversations/` 41 | * Message - `messages/` 42 | * Users (Online + Previously Connected) - `users` 43 | * User's Conversations - `users//conversations` 44 | * Conversation Messages - `conversations//messages` 45 | 46 | ## UI State 47 | * Selected Conversation 48 | * User's Conversations 49 | * Conversation Messages 50 | * Recipients List Visibility 51 | * Message Draft 52 | * Selected Recipients 53 | * Available Recipients 54 | * Body 55 | 56 | 57 | ## Dependencies 58 | ### Client-side 59 | As seen in [frontend-boilerplate](https://github.com/tj/frontend-boilerplate): 60 | - [x] [Webpack](https://webpack.github.io) 61 | - [x] [React](https://facebook.github.io/react/) 62 | - [x] [Redux](https://github.com/rackt/redux) 63 | - [x] [Babel](https://babeljs.io/) 64 | - [x] [Autoprefixer](https://github.com/postcss/autoprefixer) 65 | - [x] [PostCSS](https://github.com/postcss/postcss) 66 | - [x] [CSS modules](https://github.com/outpunk/postcss-modules) 67 | - [x] [Rucksack](http://simplaio.github.io/rucksack/docs) 68 | - [x] [React Router Redux](https://github.com/rackt/react-router-redux) 69 | - [x] [Redux DevTools Extension](https://github.com/zalmoxisus/redux-devtools-extension) 70 | 71 | ### Server-side 72 | - [x] [Deepstream.io](https://deepstream.io) 73 | - [x] [Express](http://expressjs.com/) 74 | - [x] [Passport](http://passportjs.org/) 75 | 76 | ## Setup 77 | ``` 78 | $ npm install 79 | ``` 80 | 81 | ## Running 82 | ``` 83 | $ npm start 84 | ``` 85 | 86 | # License 87 | MIT 88 | -------------------------------------------------------------------------------- /src/server/auth.js: -------------------------------------------------------------------------------- 1 | var path = require('path'); 2 | var express = require('express'); 3 | var app = express(); 4 | var port = process.env.PORT || 8001; 5 | var mongoose = require('mongoose'); 6 | var passport = require('passport'); 7 | var cookieParser = require('cookie-parser'); 8 | var cookieSession = require('cookie-session'); 9 | var flash = require('connect-flash'); 10 | 11 | var morgan = require('morgan'); 12 | var bodyParser = require('body-parser'); 13 | var session = require('express-session'); 14 | var mongoStore = require('connect-mongo')(session); 15 | var passportConfigurer = require('../../config/passport'); 16 | var cookieSecret = require('../../config/auth').cookieSecret; 17 | 18 | var authServer = { 19 | start: function () { 20 | var configDB = require('../../config/database.js'); 21 | 22 | // configuration =============================================================== 23 | // Get Mongo credentials 24 | var databaseUrl = 'mongodb://' + configDB.host + '/' + configDB.name; 25 | mongoose.connect(databaseUrl); // connect to our database 26 | 27 | passportConfigurer(passport); 28 | 29 | // set up our express application 30 | app.use(morgan('dev')); // log every request to the console 31 | app.use(bodyParser()); // get information from html forms 32 | 33 | app.set('view engine', 'ejs'); // set up jade for templating 34 | 35 | // required for passport 36 | app.use(cookieParser()); 37 | app.use(cookieSession({ 38 | key: 'imsg-sess', 39 | secret: cookieSecret, 40 | httpOnly: false 41 | })); 42 | app.use(function (req, res, next) { 43 | console.log(req.session); 44 | console.log(req.cookies); 45 | console.log(req.signedCookies); 46 | next(); 47 | }); 48 | 49 | // use passport session 50 | app.use(passport.initialize()); 51 | app.use(passport.session()); 52 | app.use(flash()); // use connect-flash for flash messages stored in session 53 | 54 | // routes ====================================================================== 55 | require('./routes.js')(app, passport); // load our routes and pass in our app and fully configured passport 56 | 57 | app.use(express.static(path.join(__dirname, '../../static'))); 58 | 59 | // launch ====================================================================== 60 | app.listen(port); 61 | console.log('Serving app on port ' + port); 62 | } 63 | }; 64 | 65 | module.exports = authServer; 66 | -------------------------------------------------------------------------------- /src/client/components/ConversationThread/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { connect } from 'react-redux' 4 | import Message from '../Message' 5 | import App from '../../containers/App' 6 | //import MessageStatus from '../MessageStatus' 7 | //import TypingIndicator from '../TypingIndicator' 8 | import style from './style.css' 9 | 10 | class ConversationThread extends Component { 11 | componentWillMount() { 12 | setTimeout(() => { 13 | this.threadContainer.scrollTop = this.threadList.scrollHeight; 14 | }, 1500) 15 | } 16 | 17 | componentWillReceiveProps(nextProps) { 18 | const { profile, selectedConversation } = nextProps 19 | if (profile.username && selectedConversation) { 20 | console.log("SUBSCRIBING TO: ", selectedConversation) 21 | this.conversationRecord = App.ds.record.getRecord(selectedConversation) 22 | this.conversationRecord.whenReady(() => { 23 | var messages = this.conversationRecord.get('messages') 24 | this.setState({ messages: messages }) 25 | this.threadContainer.scrollTop = this.threadList.scrollHeight; 26 | 27 | this.conversationRecord.subscribe('messages', (messages) => { 28 | this.setState({ messages: messages }) 29 | setTimeout(() => { 30 | this.threadContainer.scrollTop = this.threadList.scrollHeight; 31 | }, 100) 32 | }) 33 | }) 34 | } 35 | } 36 | 37 | componentWillUnmount() { 38 | if (this.conversationRecord) { 39 | this.conversationRecord.unsubscribe('messages') 40 | } 41 | } 42 | 43 | render() { 44 | const { newConversation, conversation, onSendMessage, onReceivedMessage, onReadMessage, onStartTyping } = this.props 45 | 46 | var messages = this.state ? this.state.messages : [] 47 | if (!messages.length && conversation) { 48 | messages = conversation.messages 49 | } 50 | console.log('~~~ b', messages.length); 51 | var messageItems = [] 52 | if (conversation && !newConversation) { 53 | for (let index in messages) { 54 | var message = messages[index] 55 | messageItems.push() 56 | } 57 | } 58 | return ( 59 |
    this.threadContainer = c} className={style.threadContainer}> 60 |
    this.paddingItem = c} className={style.listPadder}>
    61 |
      this.threadList = c}>{messageItems}
    62 |
    63 | ) 64 | } 65 | } 66 | 67 | function mapStateToProps(state) { 68 | return { 69 | newConversation: state.newConversation, 70 | profile: state.profile, 71 | selectedConversation: state.selectedConversation 72 | } 73 | } 74 | 75 | export default connect( 76 | mapStateToProps 77 | )(ConversationThread) 78 | -------------------------------------------------------------------------------- /src/client/containers/App/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import * as UsersActions from '../../actions/users' 6 | import style from './style.css' 7 | import deepstream from 'deepstream.io-client-js' 8 | import Cookie from 'js-cookie' 9 | import * as ProfileActions from '../../actions/profile' 10 | import * as ConversationActions from '../../actions/conversations' 11 | 12 | function containsObjectId(objId, list) { 13 | var i; 14 | for (i = 0; i < list.length; i++) { 15 | if (list[i] === objId) { 16 | return true; 17 | } 18 | } 19 | 20 | return false; 21 | } 22 | 23 | class App extends Component { 24 | componentWillMount() { 25 | const { dispatch } = this.props 26 | const sid = Cookie.get('imsg-sess') 27 | if (sid == undefined) { 28 | window.location = 'http://localhost:8001' 29 | return 30 | } 31 | App.ds.login({ sid: sid }, function (success, errorCode, userObj) { 32 | var userId = 'users/' + userObj.username 33 | console.log('<<< userId', userId) 34 | var userRecord = App.ds.record.getRecord(userId) 35 | userRecord.set(userObj) 36 | dispatch(ProfileActions.setUsername(userObj.username)) 37 | 38 | var usersList = App.ds.record.getList('users') 39 | usersList.subscribe(function onUsersChange(updatedUsersList) { 40 | console.log('List of users is now', updatedUsersList) 41 | 42 | if (!containsObjectId(userId, updatedUsersList)) { 43 | usersList.addEntry(userId) 44 | } 45 | dispatch(UsersActions.updateUserList(updatedUsersList)) 46 | }) 47 | usersList.addEntry(userId) 48 | 49 | var conversationListId = userId + '/conversations' 50 | console.log('<<< conversationListId', conversationListId) 51 | var conversations = App.ds.record.getList(conversationListId) 52 | conversations.subscribe(function onConversationsChange(updatedConversations) { 53 | console.log( 'List of my conversations is now', updatedConversations ) 54 | dispatch(ConversationActions.updateConversationList(updatedConversations)) 55 | 56 | updatedConversations.forEach( function( id ) { 57 | var conversationRecord = App.ds.record.getRecord( id ); 58 | conversationRecord.whenReady( function () { 59 | var conversationObj = conversationRecord.get() 60 | dispatch(ConversationActions.addConversation(conversationObj)) 61 | }); 62 | }) 63 | }) 64 | }) 65 | } 66 | 67 | render() { 68 | const { children } = this.props 69 | return ( 70 |
    71 | {this.props.children} 72 |
    73 | ) 74 | } 75 | } 76 | 77 | if (!App.ds) { 78 | App.ds = deepstream('localhost:6020') 79 | } 80 | 81 | export default connect()(App) 82 | -------------------------------------------------------------------------------- /src/client/components/ViewConversation/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | import { browserHistory } from 'react-router' 6 | 7 | import Conversations from '../../components/Conversations' 8 | import Conversation from '../../components/Conversation' 9 | import * as SelectedConversationActions from '../../actions/selectedConversation' 10 | import * as NewConversationActions from '../../actions/newConversation' 11 | import * as DraftActions from '../../actions/draft' 12 | import App from '../../containers/App' 13 | import style from './style.css' 14 | 15 | class ViewConversation extends Component { 16 | componentWillMount() { 17 | const { params, conversationsById, newConversationActions} = this.props 18 | const selectedConversation = 'conversations/'+params.conversationId 19 | this.setState({ 20 | selectedConversation: selectedConversation 21 | }); 22 | newConversationActions.newConversation(false) 23 | } 24 | 25 | componentDidMount() { 26 | const { params, conversationsById} = this.props 27 | const selectedConversation = 'conversations/'+params.conversationId 28 | this.setState({ 29 | selectedConversation: selectedConversation 30 | }); 31 | } 32 | 33 | componentWillReceiveProps(nextProps) { 34 | const { actions, draftActions, conversationsById } = this.props 35 | const selectedConversation = 'conversations/'+nextProps.params.conversationId 36 | console.log('<<< SWITCHING TO: ', selectedConversation); 37 | actions.viewConversation(selectedConversation) 38 | this.setState({ 39 | selectedConversation: selectedConversation 40 | }); 41 | } 42 | 43 | render() { 44 | const { params, conversationsById } = this.props 45 | console.log('state', this.state.selectedConversation); 46 | const selectedConversation = this.state.selectedConversation 47 | const conversation = selectedConversation? conversationsById[selectedConversation] : undefined 48 | console.log('<<< RENDERING CONV: ', conversation); 49 | return ( 50 |
    51 | 52 | 53 |
    54 | ) 55 | } 56 | } 57 | 58 | function mapStateToProps(state) { 59 | var conversationsState = state.conversations 60 | return { 61 | selectedConversation: state.selectedConversation, 62 | conversationsById: conversationsState.conversationsById, 63 | conversations: conversationsState.conversations 64 | } 65 | } 66 | 67 | function mapDispatchToProps(dispatch) { 68 | return { 69 | actions: bindActionCreators(SelectedConversationActions, dispatch), 70 | draftActions: bindActionCreators(DraftActions, dispatch), 71 | newConversationActions: bindActionCreators(NewConversationActions, dispatch) 72 | } 73 | } 74 | 75 | export default connect( 76 | mapStateToProps, 77 | mapDispatchToProps 78 | )(ViewConversation) 79 | -------------------------------------------------------------------------------- /src/client/containers/ConversationThreadContainer/index.js: -------------------------------------------------------------------------------- 1 | 2 | import React, { Component } from 'react' 3 | import { bindActionCreators } from 'redux' 4 | import { connect } from 'react-redux' 5 | 6 | import ConversationHeader from '../../components/ConversationHeader' 7 | import ConversationThread from '../../components/ConversationThread' 8 | import MessageInput from '../../components/MessageInput' 9 | import MessageRecipients from '../../components/MessageRecipients' 10 | import RecipientsListContainer from '..//RecipientsListContainer' 11 | import * as ConversationActions from '../../actions/conversations' 12 | import * as DraftActions from '../../actions/draft' 13 | import style from './style.css' 14 | 15 | class ConversationThreadContainer extends Component { 16 | componentWillReceiveProps(nextProps) { 17 | const { newConversation, draftActions, profile, conversation, selectedConversation, conversationsById, showRecipientsList, actions } = nextProps 18 | var conv = conversationsById[selectedConversation] || conversation 19 | if (conv && conv.participants) { 20 | var filtered = conv.participants.filter(function (participant) { 21 | if (participant !== 'users/'+profile.username) { 22 | return true 23 | } 24 | return false 25 | }) 26 | draftActions.clearDraft() 27 | if (!newConversation) { 28 | for (let userId of filtered) { 29 | console.log('RECIPIENT:', conv.id, userId); 30 | console.log(nextProps); 31 | draftActions.addRecipient(userId.split('/')[1]) 32 | } 33 | } 34 | } 35 | } 36 | 37 | render() { 38 | const { conversation, selectedConversation, conversationsById, showRecipientsList, actions } = this.props 39 | console.log('SELECTED CONVERSATION: ', selectedConversation); 40 | var conv = conversationsById[selectedConversation] || conversation 41 | return ( 42 |
    43 | 44 | 45 | 46 | 52 | 53 |
    54 | ) 55 | } 56 | } 57 | 58 | function mapStateToProps(state) { 59 | var conversationsState = state.conversations 60 | return { 61 | newConversation: state.newConversation, 62 | profile: state.profile, 63 | selectedConversation: state.selectedConversation, 64 | conversationsById: conversationsState.conversationsById, 65 | showRecipientsList: state.recipientsList 66 | } 67 | } 68 | 69 | function mapDispatchToProps(dispatch) { 70 | return { 71 | actions: bindActionCreators(ConversationActions, dispatch), 72 | draftActions: bindActionCreators(DraftActions, dispatch) 73 | } 74 | } 75 | 76 | export default connect( 77 | mapStateToProps, 78 | mapDispatchToProps 79 | )(ConversationThreadContainer) 80 | -------------------------------------------------------------------------------- /src/client/actions/draft.js: -------------------------------------------------------------------------------- 1 | 2 | import { createAction } from 'redux-actions' 3 | import { browserHistory } from 'react-router' 4 | import * as ConversationActions from './conversations' 5 | import App from '../containers/App' 6 | 7 | export const addRecipient = createAction('add recipient') 8 | export const removeRecipient = createAction('remove recipient') 9 | export const addMessageBody = createAction('add message body') 10 | export const clearDraft = createAction('new draft') 11 | 12 | export function sendDraft(body) { 13 | return function (dispatch, getState) { 14 | const { draft, profile, conversations } = getState() 15 | 16 | // Create message record 17 | var messageId = 'messages/' + App.ds.getUid() 18 | var messageRecord = App.ds.record.getRecord(messageId) 19 | var authorId = 'users/' + profile.username 20 | var messageObj = { 21 | id: messageId, 22 | author: authorId, 23 | body: body, 24 | time: Date.now() 25 | } 26 | messageRecord.set(messageObj) 27 | 28 | // Create conversation record 29 | var participants = [...draft.recipients, authorId].sort() 30 | var conversationId = 'conversations/' + new Buffer(JSON.stringify(participants)).toString('base64') 31 | var existingConversation = conversations.conversationsById[conversationId] 32 | var conversationRecord = App.ds.record.getRecord(conversationId) 33 | conversationRecord.whenReady(function () { 34 | var conv = conversationRecord.get() 35 | const isNewConv = !conv || !conv.messages || conv.messages.length == 0 36 | if (isNewConv) { 37 | // If new conversation, assign message array 38 | var conversationObj = { 39 | id: conversationId, 40 | participants: participants, 41 | lastMessage: body, 42 | messages: [messageId] 43 | } 44 | conversationRecord.set(conversationObj) 45 | } else { 46 | // Otherwise, append messageId 47 | var messagesList = [...conv.messages, messageId] 48 | conversationRecord.set('messages', messagesList) 49 | conversationRecord.set('lastMessage', body) 50 | } 51 | 52 | // Add conversation to participant conv lists 53 | for (let index in participants) { 54 | var listId = participants[index] + '/conversations' 55 | var participantConversationList = App.ds.record.getList(listId) 56 | participantConversationList.whenReady(function () { 57 | var entries = participantConversationList.getEntries() 58 | if (entries.indexOf(conversationId) == -1) { 59 | participantConversationList.addEntry(conversationId) 60 | } 61 | }) 62 | } 63 | 64 | // Add message to store 65 | dispatch(ConversationActions.addMessage(Object.assign({ 66 | conversationId: conversationId 67 | }, messageObj ))) 68 | 69 | if (isNewConv) { 70 | // If new conversation, navigate to conversation 71 | browserHistory.push('/' + conversationId) 72 | } 73 | }) 74 | 75 | dispatch(clearDraft()) 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /src/client/containers/App/style.css: -------------------------------------------------------------------------------- 1 | /** Ultra Light */ 2 | @font-face { 3 | font-family: "San Francisco"; 4 | font-weight: 100; 5 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-ultralight-webfont.woff"); 6 | } 7 | 8 | /** Thin */ 9 | @font-face { 10 | font-family: "San Francisco"; 11 | font-weight: 200; 12 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-thin-webfont.woff"); 13 | } 14 | 15 | /** Regular */ 16 | @font-face { 17 | font-family: "San Francisco"; 18 | font-weight: 400; 19 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-regular-webfont.woff"); 20 | } 21 | 22 | /** Medium */ 23 | @font-face { 24 | font-family: "San Francisco"; 25 | font-weight: 500; 26 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-medium-webfont.woff"); 27 | } 28 | 29 | /** Semi Bold */ 30 | @font-face { 31 | font-family: "San Francisco"; 32 | font-weight: 600; 33 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-semibold-webfont.woff"); 34 | } 35 | 36 | /** Bold */ 37 | @font-face { 38 | font-family: "San Francisco"; 39 | font-weight: 700; 40 | src: url("https://applesocial.s3.amazonaws.com/assets/styles/fonts/sanfrancisco/sanfranciscodisplay-bold-webfont.woff"); 41 | } 42 | 43 | 44 | html, 45 | body { 46 | margin: 0; 47 | padding: 0; 48 | } 49 | 50 | button { 51 | margin: 0; 52 | padding: 0; 53 | border: 0; 54 | background: none; 55 | font-size: 100%; 56 | vertical-align: baseline; 57 | font-family: inherit; 58 | font-weight: inherit; 59 | color: inherit; 60 | appearance: none; 61 | font-smoothing: antialiased; 62 | } 63 | 64 | body { 65 | font: 14px 'San Francisco', 'Helvetica Neue', Helvetica, Arial, sans-serif; 66 | line-height: 1.4em; 67 | background: #fefefe; 68 | color: #4d4d4d; 69 | position: absolute; 70 | top: 0; 71 | left: 0; 72 | right: 0; 73 | bottom: 0; 74 | margin: 0; 75 | overflow: hidden; 76 | -webkit-font-smoothing: antialiased; 77 | -moz-font-smoothing: antialiased; 78 | -ms-font-smoothing: antialiased; 79 | font-smoothing: antialiased; 80 | } 81 | 82 | button, 83 | input[type="checkbox"] { 84 | outline: none; 85 | } 86 | 87 | .normal { 88 | background: #fff; 89 | margin: 200px 0 40px 0; 90 | position: relative; 91 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 92 | 0 25px 50px 0 rgba(0, 0, 0, 0.1); 93 | } 94 | 95 | .normal input::-webkit-input-placeholder { 96 | font-style: italic; 97 | font-weight: 300; 98 | color: #e6e6e6; 99 | } 100 | 101 | .normal input::-moz-placeholder { 102 | font-style: italic; 103 | font-weight: 300; 104 | color: #e6e6e6; 105 | } 106 | 107 | .normal input::input-placeholder { 108 | font-style: italic; 109 | font-weight: 300; 110 | color: #e6e6e6; 111 | } 112 | 113 | .normal h1 { 114 | position: absolute; 115 | top: -155px; 116 | width: 100%; 117 | font-size: 100px; 118 | font-weight: 100; 119 | text-align: center; 120 | color: rgba(175, 47, 47, 0.15); 121 | -webkit-text-rendering: optimizeLegibility; 122 | -moz-text-rendering: optimizeLegibility; 123 | -ms-text-rendering: optimizeLegibility; 124 | text-rendering: optimizeLegibility; 125 | } 126 | -------------------------------------------------------------------------------- /config/passport.js: -------------------------------------------------------------------------------- 1 | var LocalStrategy = require('passport-local').Strategy; 2 | var User = require('../src/server/models/user'); 3 | 4 | // expose this function to our app using module.exports 5 | module.exports = function(passport) { 6 | 7 | // ========================================================================= 8 | // passport session setup ================================================== 9 | // ========================================================================= 10 | // required for persistent login sessions 11 | // passport needs ability to serialize and unserialize users out of session 12 | 13 | // used to serialize the user for the session 14 | passport.serializeUser(function(user, done) { 15 | done(null, user.id); 16 | }); 17 | 18 | // used to deserialize the user 19 | passport.deserializeUser(function(id, done) { 20 | User.findById(id, function(err, user) { 21 | done(err, user); 22 | }); 23 | }); 24 | 25 | // ========================================================================= 26 | // LOCAL SIGNUP ============================================================ 27 | // ========================================================================= 28 | // we are using named strategies since we have one for login and one for signup 29 | // by default, if there was no name, it would just be called 'local' 30 | 31 | passport.use('local-signup', new LocalStrategy({ 32 | // by default, local strategy uses username and password, we will override with email 33 | usernameField : 'username', 34 | passwordField : 'password', 35 | passReqToCallback : true // allows us to pass back the entire request to the callback 36 | }, 37 | function(req, username, password, done) { 38 | 39 | // asynchronous 40 | // User.findOne wont fire unless data is sent back 41 | process.nextTick(function() { 42 | 43 | // find a user whose email is the same as the forms email 44 | // we are checking to see if the user trying to login already exists 45 | User.findOne({ 'local.username' : username }, function(err, user) { 46 | // if there are any errors, return the error 47 | if (err) { 48 | return done(err); 49 | } 50 | 51 | // check to see if theres already a user with that email 52 | if (user) { 53 | return done(null, false, req.flash('signupMessage', 'That username is already taken.')); 54 | } else { 55 | 56 | // if there is no user with that email 57 | // create the user 58 | var newUser = new User(); 59 | 60 | // set the user's local credentials 61 | newUser.local.username = username; 62 | newUser.local.password = newUser.generateHash(password); 63 | 64 | // save the user 65 | newUser.save(function(err) { 66 | if (err) { 67 | throw err; 68 | } 69 | return done(null, newUser); 70 | }); 71 | } 72 | 73 | }); 74 | 75 | }); 76 | 77 | })); 78 | 79 | // ========================================================================= 80 | // LOCAL LOGIN ============================================================= 81 | // ========================================================================= 82 | // we are using named strategies since we have one for login and one for signup 83 | // by default, if there was no name, it would just be called 'local' 84 | 85 | passport.use('local-login', new LocalStrategy({ 86 | usernameField : 'username', 87 | passwordField : 'password', 88 | passReqToCallback : true // allows us to pass back the entire request to the callback 89 | }, 90 | function(req, username, password, done) { // callback with email and password from our form 91 | // find a user whose email is the same as the forms email 92 | // we are checking to see if the user trying to login already exists 93 | User.findOne({ 'local.username' : username }, function(err, user) { 94 | // if there are any errors, return the error before anything else 95 | if (err) { 96 | return done(err); 97 | } 98 | 99 | // if no user is found, return the message 100 | if (!user) 101 | return done(null, false, req.flash('loginMessage', 'No user found.')); // req.flash is the way to set flashdata using connect-flash 102 | 103 | // if the user is found but the password is wrong 104 | if (!user.validPassword(password)) 105 | return done(null, false, req.flash('loginMessage', 'Oops! Wrong password.')); // create the loginMessage and save it to session as flashdata 106 | 107 | // all is well, return successful user 108 | return done(null, user); 109 | }); 110 | 111 | })); 112 | 113 | }; 114 | --------------------------------------------------------------------------------