├── .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 |
21 | );
22 | }
23 |
24 | _onChange(event, value) {
25 | this.setState({text: event.target.value});
26 | }
27 |
28 | _onKeyDown(event) {
29 | if (event.keyCode === ENTER_KEY_CODE) {
30 | event.preventDefault();
31 | let text = this.state.text.trim();
32 | if (text) {
33 | this.props.actions.postNewMessage(text, this.props.threadID);
34 | }
35 | this.setState({text: ''});
36 | }
37 | }
38 |
39 | };
40 |
41 | MessageComposer.propTypes = {
42 | threadID: PropTypes.string.isRequired
43 | };
44 |
45 | export default MessageComposer;
46 |
--------------------------------------------------------------------------------
/js/components/MessageListItem.react.js:
--------------------------------------------------------------------------------
1 | import React, { PropTypes } from 'react';
2 |
3 | class MessageListItem extends React.Component {
4 |
5 | render() {
6 | let {message} = this.props;
7 | return (
8 |
9 | {message.authorName}
10 |
11 | {message.date.toLocaleTimeString()}
12 |
13 | {message.text}
14 |
15 | );
16 | }
17 |
18 | };
19 |
20 | MessageListItem.propTypes = {
21 | message: PropTypes.object
22 | };
23 |
24 | export default MessageListItem;
25 |
--------------------------------------------------------------------------------
/js/components/MessageSection.react.js:
--------------------------------------------------------------------------------
1 | import React from 'react';
2 | import MessageComposer from './MessageComposer.react';
3 | import MessageListItem from './MessageListItem.react';
4 |
5 | export default class MessageSection extends React.Component {
6 |
7 | componentDidMount() {
8 | this._scrollToBottom();
9 | }
10 |
11 | render() {
12 | const { currentThread, messages } = this.props;
13 |
14 | if (currentThread) {
15 | let messageListItems = currentThread.messages.map((messageID) => {
16 | let message = messages[messageID];
17 | return (
18 |
22 | );
23 | });
24 |
25 | return (
26 |
27 |
{currentThread.threadName}
28 |
29 | {messageListItems}
30 |
31 |
35 |
36 | );
37 | } else {
38 | return ;
39 | }
40 | }
41 |
42 | componentDidUpdate() {
43 | this._scrollToBottom();
44 | }
45 |
46 | _scrollToBottom() {
47 | let ul = this.refs.messageList;
48 | if (ul) {
49 | ul.scrollTop = ul.scrollHeight;
50 | }
51 | }
52 |
53 | };
54 |
--------------------------------------------------------------------------------
/js/components/ThreadListItem.react.js:
--------------------------------------------------------------------------------
1 | import * as Actions from '../actions';
2 | import React, { PropTypes } from 'react';
3 | import classNames from 'classnames';
4 |
5 | class ThreadListItem extends React.Component {
6 |
7 | render() {
8 | let thread = this.props.thread;
9 | let lastMessage = this.props.lastMessage;
10 |
11 | return (
12 |
18 | {thread.threadName}
19 |
20 | {lastMessage.date.toLocaleTimeString()}
21 |
22 |
23 | {lastMessage.text}
24 |
25 |
26 | );
27 | }
28 |
29 | _onClick() {
30 | this.props.actions.clickThread(this.props.thread.id);
31 | }
32 |
33 | };
34 |
35 | ThreadListItem.propTypes = {
36 | thread: PropTypes.object.isRequired,
37 | lastMessage: PropTypes.object.isRequired,
38 | currentThreadID: PropTypes.string.isRequired
39 | };
40 |
41 | export default ThreadListItem;
42 |
--------------------------------------------------------------------------------
/js/components/ThreadSection.react.js:
--------------------------------------------------------------------------------
1 | import React, { Component } from 'react';
2 | import ThreadListItem from '../components/ThreadListItem.react';
3 |
4 | export default class ThreadSection extends Component {
5 |
6 | render() {
7 | let unreadCount = 0;
8 |
9 | let threadListItems = Object.keys(this.props.threads).map(id => {
10 | let thread = this.props.threads[id];
11 | let lastMessage = this.props.messages[thread.lastMessage];
12 | if (!lastMessage.isRead) {
13 | unreadCount += 1;
14 | }
15 | return (
16 |
23 | );
24 | });
25 |
26 | let unread =
27 | unreadCount === 0 ? null : Unread threads: {unreadCount};
28 | return (
29 |
30 |
31 | {unread}
32 |
33 |
34 | {threadListItems}
35 |
36 |
37 | );
38 | }
39 |
40 | };
41 |
--------------------------------------------------------------------------------
/js/constants/ChatConstants.js:
--------------------------------------------------------------------------------
1 | import keyMirror from 'keymirror';
2 |
3 | export default {
4 |
5 | ActionTypes: keyMirror({
6 | CLICK_THREAD: null,
7 | CREATE_MESSAGE: null,
8 | RECEIVE_RAW_CREATED_MESSAGE: null,
9 | RAW_MESSAGES_REQUEST: null,
10 | RECEIVE_RAW_MESSAGES: null
11 | })
12 |
13 | };
14 |
--------------------------------------------------------------------------------
/js/reducers/currentThreadID.js:
--------------------------------------------------------------------------------
1 | import last from 'lodash/array/last';
2 | import { ActionTypes } from '../constants/ChatConstants';
3 |
4 | export default function currentThreadID(state = null, action) {
5 | switch (action.type) {
6 | case ActionTypes.CLICK_THREAD:
7 | return action.threadID;
8 | case ActionTypes.RECEIVE_RAW_MESSAGES:
9 | return last(action.rawMessages).threadID;
10 | default:
11 | return state;
12 | }
13 | }
14 |
--------------------------------------------------------------------------------
/js/reducers/index.js:
--------------------------------------------------------------------------------
1 | import { combineReducers } from 'redux';
2 | import currentThreadID from './currentThreadID';
3 | import messages from './messages';
4 | import threads from './threads';
5 |
6 | /**
7 | * State shape
8 | *
9 | * {
10 | * currentThreadID,
11 | * messages: {
12 | * id: {
13 | * id,
14 | * threadID,
15 | * threadName,
16 | * authorName,
17 | * text,
18 | * timestamp,
19 | * isRead
20 | * }
21 | * },
22 | * threads: {
23 | * id: {
24 | * id,
25 | * threadName,
26 | * lastMessage,
27 | * messages: [id],
28 | * }
29 | * }
30 | * }
31 | */
32 |
33 | export default combineReducers({
34 | currentThreadID,
35 | messages,
36 | threads
37 | });
38 |
--------------------------------------------------------------------------------
/js/reducers/messages.js:
--------------------------------------------------------------------------------
1 | import last from 'lodash/array/last';
2 | import indexBy from 'lodash/collection/indexBy';
3 | import { compose } from 'redux';
4 | import u from 'updeep';
5 | import { ActionTypes } from '../constants/ChatConstants';
6 | import * as ChatMessageUtils from '../utils/ChatMessageUtils';
7 |
8 | export default function messages(state = {}, action) {
9 |
10 | switch (action.type) {
11 |
12 | case ActionTypes.CLICK_THREAD: {
13 | let markRead = u({isRead: true});
14 | return u(
15 | u.map(m => m.threadID === action.threadID ? markRead(m) : m),
16 | state);
17 | }
18 |
19 | case ActionTypes.CREATE_MESSAGE: {
20 | return {
21 | ...state,
22 | [action.message.id]: action.message
23 | };
24 | }
25 |
26 | case ActionTypes.RECEIVE_RAW_CREATED_MESSAGE: {
27 | let message = ChatMessageUtils.convertRawMessage(action.rawMessage);
28 | message.isRead = true;
29 | return compose(
30 | u({[message.id]: message}),
31 | u(u.omit(action.tempMessageID))
32 | )(state);
33 | }
34 |
35 | case ActionTypes.RECEIVE_RAW_MESSAGES: {
36 | let lastThreadID = last(action.rawMessages).threadID;
37 | let messages = indexBy(action.rawMessages, 'id');
38 | let formatMessage = u.map(message => u({
39 | isRead: message.threadID === lastThreadID,
40 | date: new Date(message.timestamp)
41 | }, message));
42 | let updated = u(formatMessage, messages);
43 | return {...state, ...updated};
44 | }
45 |
46 | default:
47 | return state;
48 |
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/js/reducers/threads.js:
--------------------------------------------------------------------------------
1 | import last from 'lodash/array/last';
2 | import groupBy from 'lodash/collection/groupBy';
3 | import pluck from 'lodash/collection/pluck';
4 | import u from 'updeep';
5 | import { ActionTypes } from '../constants/ChatConstants';
6 |
7 | export default function threads(state = {}, action) {
8 |
9 | switch (action.type) {
10 |
11 | case ActionTypes.CREATE_MESSAGE: {
12 | let message = action.message;
13 | return u({
14 | [message.threadID]: {
15 | lastMessage: message.id,
16 | messages: arr => arr.concat([message.id])
17 | }
18 | }, state);
19 | }
20 |
21 | case ActionTypes.RECEIVE_RAW_CREATED_MESSAGE: {
22 | let {rawMessage: message, tempMessageID} = action;
23 | return u({
24 | [message.threadID]: {
25 | lastMessage: message.id,
26 | messages: arr =>
27 | u.reject(id => id === tempMessageID, arr).concat([message.id])
28 | }
29 | }, state);
30 | }
31 |
32 | case ActionTypes.RECEIVE_RAW_MESSAGES: {
33 | let threads = groupBy(action.rawMessages, 'threadID');
34 | let updated = u(
35 | u.map(messages => {
36 | return {
37 | id: messages[0].threadID,
38 | threadName: messages[0].threadName,
39 | lastMessage: last(messages).id,
40 | messages: pluck(messages, 'id')
41 | };
42 | }),
43 | threads);
44 | return {...state, ...updated};
45 | }
46 |
47 | default:
48 | return state;
49 | }
50 |
51 | }
52 |
--------------------------------------------------------------------------------
/js/store/configureStore.js:
--------------------------------------------------------------------------------
1 | import { createStore, applyMiddleware } from 'redux';
2 | import thunkMiddleware from 'redux-thunk';
3 | import rootReducer from '../reducers';
4 |
5 | const createStoreWithMiddleware = applyMiddleware(thunkMiddleware)(createStore);
6 |
7 | export default function configureStore(initialState) {
8 | return createStoreWithMiddleware(rootReducer, initialState);
9 | }
10 |
--------------------------------------------------------------------------------
/js/utils/ChatMessageUtils.js:
--------------------------------------------------------------------------------
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 | export function convertRawMessage(rawMessage, currentThreadID) {
14 | return {
15 | ...rawMessage,
16 | date: new Date(rawMessage.timestamp),
17 | isRead: rawMessage.threadID === currentThreadID
18 | };
19 | };
20 |
21 | export function getCreatedMessageData(text, currentThreadID) {
22 | var timestamp = Date.now();
23 | return {
24 | id: 'm_' + timestamp,
25 | threadID: currentThreadID,
26 | authorName: 'Bill', // hard coded for the example
27 | date: new Date(timestamp),
28 | text: text,
29 | isRead: true
30 | };
31 | };
32 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "redux-chat",
3 | "version": "0.0.1",
4 | "description": "Example Flux chat application with Redux",
5 | "repository": "https://github.com/d6u/redux-chat.git",
6 | "author": "Daiwei Lu",
7 | "main": "js/app.js",
8 | "dependencies": {
9 | "classnames": "^2.1.3",
10 | "keymirror": "~0.1.0",
11 | "lodash": "^3.10.1",
12 | "react": "^0.14.0-rc1",
13 | "react-dom": "^0.14.0-rc1",
14 | "react-redux": "^2.1.2",
15 | "redux": "^3.0.0",
16 | "redux-thunk": "^0.1.0",
17 | "updeep": "0.10.x"
18 | },
19 | "devDependencies": {
20 | "babel-core": "^5.8.24",
21 | "babel-loader": "^5.3.2",
22 | "babel-runtime": "^5.8.24",
23 | "webpack": "^1.12.1"
24 | },
25 | "scripts": {
26 | "start": "webpack -w",
27 | "build": "webpack"
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/webpack.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | entry: './js/app.js',
3 | output: {
4 | filename: 'bundle.js',
5 | path: './js',
6 | },
7 | devtool: 'source-map',
8 |
9 | module: {
10 | loaders: [
11 | {
12 | test: /\.js?$/,
13 | exclude: /(node_modules|bower_components)/,
14 | loader: 'babel?optional[]=runtime'
15 | }
16 | ]
17 | }
18 | };
19 |
--------------------------------------------------------------------------------