├── .gitignore ├── README.md ├── css └── chatapp.css ├── index.html ├── js ├── ChatExampleDataServer.js ├── actions.js ├── app.js ├── components │ ├── ChatApp.react.js │ ├── MessageComposer.react.js │ ├── MessageListItem.react.js │ ├── MessageSection.react.js │ ├── ThreadListItem.react.js │ └── ThreadSection.react.js ├── constants │ └── ChatConstants.js ├── reducers │ ├── currentThreadID.js │ ├── index.js │ ├── messages.js │ └── threads.js ├── store │ └── configureStore.js └── utils │ └── ChatMessageUtils.js ├── package.json └── webpack.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | *.log 2 | node_modules 3 | .DS_Store 4 | js/bundle.js 5 | js/bundle.js.map 6 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Redux Chat Example 2 | 3 | **This was base on [Flux Chat example](https://github.com/d6u/flux-chat),** 4 | **which was originated from Facebook Flux Chat example, but with Redux as its** 5 | **Flux implementation.** 6 | 7 | ## Redux 8 | 9 | Instead of using the official Facebook Flux dispatcher, Redux Chat example is 10 | using [Redux](https://github.com/rackt/redux) with no dispatcher, a singleton 11 | store, pure function action creators and reducers. Please see Details on 12 | official Redux repo. 13 | 14 | ## Running 15 | 16 | You must have [npm](https://www.npmjs.org/) installed on your computer. 17 | From the root project directory run these commands from the command line: 18 | 19 | `npm install` 20 | 21 | This will install all dependencies. 22 | 23 | To build the project, first run this command: 24 | 25 | `npm start` 26 | 27 | This will perform an initial build and start a watcher process that will 28 | update bundle.js with any changes you wish to make. This watcher is 29 | based on [Webpack](http://webpack.github.io/), and it transforms 30 | React's JSX syntax into standard JavaScript with [Babel](https://babeljs.io/). 31 | 32 | After starting the watcher, you can open `index.html` in your browser to 33 | open the app. 34 | -------------------------------------------------------------------------------- /css/chatapp.css: -------------------------------------------------------------------------------- 1 | /** 2 | * This file is provided by Facebook for testing and evaluation purposes 3 | * only. Facebook reserves all rights not expressly granted. 4 | * 5 | * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 6 | * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 7 | * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL 8 | * FACEBOOK BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN 9 | * AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN 10 | * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 11 | */ 12 | 13 | .chatapp { 14 | font-family: 'Muli', 'Helvetica Neue', helvetica, arial; 15 | max-width: 760px; 16 | margin: 20px auto; 17 | overflow: hidden; 18 | } 19 | 20 | .message-list, .thread-list { 21 | border: 1px solid #ccf; 22 | font-size: 16px; 23 | height: 400px; 24 | margin: 0; 25 | overflow-y: auto; 26 | padding: 0; 27 | } 28 | 29 | .message-section { 30 | float: right; 31 | width: 65%; 32 | } 33 | 34 | .thread-section { 35 | float: left; 36 | width: 32.5%; 37 | } 38 | 39 | .message-thread-heading, 40 | .thread-count { 41 | height: 40px; 42 | margin: 0; 43 | } 44 | 45 | .message-list-item, .thread-list-item { 46 | list-style: none; 47 | padding: 12px 14px 14px; 48 | } 49 | 50 | .thread-list-item { 51 | border-bottom: 1px solid #ccc; 52 | cursor: pointer; 53 | } 54 | 55 | .thread-list:hover .thread-list-item:hover { 56 | background-color: #f8f8ff; 57 | } 58 | 59 | .thread-list:hover .thread-list-item { 60 | background-color: #fff; 61 | } 62 | 63 | .thread-list-item.active, 64 | .thread-list:hover .thread-list-item.active, 65 | .thread-list:hover .thread-list-item.active:hover { 66 | background-color: #efefff; 67 | cursor: default; 68 | } 69 | 70 | .message-author-name, 71 | .thread-name { 72 | color: #66c; 73 | float: left; 74 | font-size: 13px; 75 | margin: 0; 76 | } 77 | 78 | .message-time, .thread-time { 79 | color: #aad; 80 | float: right; 81 | font-size: 12px; 82 | } 83 | 84 | .message-text, .thread-last-message { 85 | clear: both; 86 | font-size: 14px; 87 | padding-top: 10px; 88 | } 89 | 90 | .message-composer { 91 | box-sizing: border-box; 92 | font-family: inherit; 93 | font-size: 14px; 94 | height: 5em; 95 | width: 100%; 96 | margin: 20px 0 0; 97 | padding: 10px; 98 | } 99 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Redux • Chat 6 | 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /js/ChatExampleDataServer.js: -------------------------------------------------------------------------------- 1 | import cloneDeep from 'lodash/lang/cloneDeep'; 2 | 3 | /** 4 | * This file contains mock methods for fetching data from server. 5 | * It uses setTimeout to simulate server latency. 6 | */ 7 | 8 | let NETWORK_LATENCY = 100; 9 | 10 | let messages = [ 11 | { 12 | id: 'm_1', 13 | threadID: 't_1', 14 | threadName: 'Jing and Bill', 15 | authorName: 'Bill', 16 | text: 'Hey Jing, want to give a Flux talk at ForwardJS?', 17 | timestamp: Date.now() - 99999 18 | }, 19 | { 20 | id: 'm_2', 21 | threadID: 't_1', 22 | threadName: 'Jing and Bill', 23 | authorName: 'Bill', 24 | text: 'Seems like a pretty cool conference.', 25 | timestamp: Date.now() - 89999 26 | }, 27 | { 28 | id: 'm_3', 29 | threadID: 't_1', 30 | threadName: 'Jing and Bill', 31 | authorName: 'Jing', 32 | text: 'Sounds good. Will they be serving dessert?', 33 | timestamp: Date.now() - 79999 34 | }, 35 | { 36 | id: 'm_4', 37 | threadID: 't_2', 38 | threadName: 'Dave and Bill', 39 | authorName: 'Bill', 40 | text: 'Hey Dave, want to get a beer after the conference?', 41 | timestamp: Date.now() - 69999 42 | }, 43 | { 44 | id: 'm_5', 45 | threadID: 't_2', 46 | threadName: 'Dave and Bill', 47 | authorName: 'Dave', 48 | text: 'Totally! Meet you at the hotel bar.', 49 | timestamp: Date.now() - 59999 50 | }, 51 | { 52 | id: 'm_6', 53 | threadID: 't_3', 54 | threadName: 'Functional Heads', 55 | authorName: 'Bill', 56 | text: 'Hey Brian, are you going to be talking about functional stuff?', 57 | timestamp: Date.now() - 49999 58 | }, 59 | { 60 | id: 'm_7', 61 | threadID: 't_3', 62 | threadName: 'Bill and Brian', 63 | authorName: 'Brian', 64 | text: 'At ForwardJS? Yeah, of course. See you there!', 65 | timestamp: Date.now() - 39999 66 | } 67 | ]; 68 | 69 | let threadNameMap = (function () { 70 | let map = {}; 71 | messages.forEach(({threadID, threadName}) => { 72 | map[threadID] = threadName; 73 | }); 74 | return map; 75 | })(); 76 | 77 | export function getMessages(callback) { 78 | setTimeout(() => { 79 | callback(cloneDeep(messages)); 80 | }, NETWORK_LATENCY); 81 | }; 82 | 83 | export function postMessage(message, callback) { 84 | let timestamp = Date.now(); 85 | let id = 'm_' + timestamp; 86 | let threadID = message.threadID; 87 | 88 | let createdMessage = { 89 | id, 90 | threadID, 91 | threadName: threadNameMap[threadID], 92 | authorName: message.authorName, 93 | text: message.text, 94 | timestamp 95 | }; 96 | 97 | messages.push(createdMessage); 98 | 99 | setTimeout(() => { 100 | callback(cloneDeep(createdMessage)); 101 | }, NETWORK_LATENCY); 102 | }; 103 | -------------------------------------------------------------------------------- /js/actions.js: -------------------------------------------------------------------------------- 1 | import { ActionTypes } from './constants/ChatConstants'; 2 | import * as ChatExampleDataServer from './ChatExampleDataServer'; 3 | import * as ChatMessageUtils from './utils/ChatMessageUtils'; 4 | 5 | export function clickThread(threadID) { 6 | return { 7 | type: ActionTypes.CLICK_THREAD, 8 | threadID: threadID 9 | }; 10 | } 11 | 12 | export function createMessage(message) { 13 | return { 14 | type: ActionTypes.CREATE_MESSAGE, 15 | message 16 | }; 17 | } 18 | 19 | export function receiveCreatedMessage(createdMessage, tempMessageID) { 20 | return { 21 | type: ActionTypes.RECEIVE_RAW_CREATED_MESSAGE, 22 | rawMessage: createdMessage, 23 | tempMessageID 24 | }; 25 | } 26 | 27 | export function requestRawMessages() { 28 | return { 29 | type: ActionTypes.RAW_MESSAGES_REQUEST 30 | }; 31 | } 32 | 33 | export function receiveAll(rawMessages) { 34 | return { 35 | type: ActionTypes.RECEIVE_RAW_MESSAGES, 36 | rawMessages: rawMessages 37 | }; 38 | } 39 | 40 | export function getAllMessages() { 41 | return dispatch => { 42 | dispatch(requestRawMessages()); 43 | ChatExampleDataServer.getMessages(messages => { 44 | dispatch(receiveAll(messages)); 45 | }); 46 | }; 47 | } 48 | 49 | export function postNewMessage(text, currentThreadID) { 50 | return dispatch => { 51 | let message = ChatMessageUtils.getCreatedMessageData(text, currentThreadID); 52 | dispatch(createMessage(message)); 53 | ChatExampleDataServer.postMessage(message, createdMessage => { 54 | dispatch(receiveCreatedMessage(createdMessage, message.id)); 55 | }); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /js/app.js: -------------------------------------------------------------------------------- 1 | // This file bootstraps the entire application. 2 | 3 | import React from 'react'; 4 | import ReactDOM from 'react-dom'; 5 | import { Provider } from 'react-redux'; 6 | import ChatApp from './components/ChatApp.react'; 7 | import * as Actions from './actions'; 8 | import configureStore from './store/configureStore'; 9 | 10 | const store = configureStore(); 11 | 12 | store.dispatch(Actions.getAllMessages()); 13 | 14 | ReactDOM.render( 15 | 16 | 17 | , 18 | document.getElementById('react') 19 | ); 20 | -------------------------------------------------------------------------------- /js/components/ChatApp.react.js: -------------------------------------------------------------------------------- 1 | import React, { Component, PropTypes } from 'react'; 2 | import { bindActionCreators } from 'redux'; 3 | import { connect } from 'react-redux'; 4 | import * as Actions from '../actions'; 5 | import MessageSection from './MessageSection.react'; 6 | import ThreadSection from './ThreadSection.react'; 7 | 8 | class ChatApp extends Component { 9 | 10 | render() { 11 | const { threads, messages, currentThreadID, dispatch } = this.props; 12 | const actions = bindActionCreators(Actions, dispatch); 13 | return ( 14 |
15 | 21 | 26 |
27 | ); 28 | } 29 | 30 | }; 31 | 32 | ChatApp.propTypes = { 33 | threads: PropTypes.object.isRequired, 34 | messages: PropTypes.object.isRequired, 35 | currentThreadID: PropTypes.string, 36 | dispatch: PropTypes.func.isRequired 37 | }; 38 | 39 | function mapStateToProps(state) { 40 | return { 41 | threads: state.threads, 42 | messages: state.messages, 43 | currentThreadID: state.currentThreadID 44 | }; 45 | } 46 | 47 | export default connect(mapStateToProps)(ChatApp); 48 | -------------------------------------------------------------------------------- /js/components/MessageComposer.react.js: -------------------------------------------------------------------------------- 1 | import React, { PropTypes } from 'react'; 2 | 3 | let ENTER_KEY_CODE = 13; 4 | 5 | class MessageComposer extends React.Component { 6 | 7 | constructor(props) { 8 | super(props); 9 | this.state = {text: ''}; 10 | } 11 | 12 | render() { 13 | return ( 14 |