<main></main>
body { padding-top: 20px; }
.row { margin-bottom: 20px; }

var RECEIVED_ROOMS = "RECEIVED_ROOMS";
var SET_ACTIVE_ROOM = "SET_ACTIVE_ROOM";
var RECEIVED_MESSAGE = "RECEIVED_MESSAGE";
var SAY = "SAY";
var SET_USERNAME = "SET_USERNAME";
var CREATE_ROOM = "CREATE_ROOM";

var server = new Firebase("https://tocode-redux-chat.firebaseio.com/");

server.child("rooms").on("value", function(snapshot) {
  store.dispatch(actions.receivedRooms(snapshot.val()));
});

var initialState = {
  rooms: {
    all: {},
  },
  messages: {
    activeRoom: undefined,
    activeRoomId: undefined,
    all: [],
  },
  username: "Anonymous",
};

var actions = {
  receivedRooms:   (res)     => ({ type: RECEIVED_ROOMS, payload: res }),
  setActiveRoom:   (room_id) => ({ type: SET_ACTIVE_ROOM, payload: room_id }),
  createRoom:      (room)    => ({ type: CREATE_ROOM, payload: room }),
  receivedMessage: (text)    => ({ type: RECEIVED_MESSAGE, payload: text }),
  say:             (text)    => ({ type: SAY, payload: { text: text, from: store.getState().username }}),  
  setUsername:     (name)    => ({ type: SET_USERNAME, payload: name }),
};

function new_message(snapshot) {
  var text = snapshot.val();
  store.dispatch(actions.receivedMessage(text));
}

function rooms(state, action) {
  switch(action.type) {
    case RECEIVED_ROOMS:      
      return Object.assign({}, state, { all: action.payload } );
          
    case CREATE_ROOM:
      setTimeout(function() {
        server.child('rooms').push({ name: action.payload, messages: [] });
      }, 0);
      
      return state;
      
    default:
      return state;
  }
}

function messages(state, action) {
  switch(action.type) {
    case RECEIVED_MESSAGE:                  
      return Object.assign({}, state, { all: state.all.concat(action.payload)});
      
    case SAY:
      setTimeout(function() {
        state.activeRoom.push( action.payload );
      }, 0);
      return state;
      
    case SET_ACTIVE_ROOM:
      if ( state.activeRoom ) {
        state.activeRoom.off();        
      }

      var activeRoom = server.child('rooms').child(action.payload).child('messages');
      setTimeout(function() {
        activeRoom.on('child_added', new_message);
      }, 0);      
      return Object.assign({}, state, { activeRoom: activeRoom, activeRoomId: action.payload, all: [] });
      
    default:
      return state;
  }  
}

function userinfo(state, action) {
  switch(action.type) {
    case SET_USERNAME:
      return action.payload;
      
    default:
      return state;      
  }
}

function myApp(state = initialState, action) {
  return {
    ...state,
    rooms: rooms(state.rooms, action),
    messages: messages(state.messages, action),
    username: userinfo(state.username, action)
  };
}

var store = Redux.createStore(myApp);


var RoomList = React.createClass({
  render: function() {
    var rooms = this.props.rooms;
    var setActiveRoom = this.props.setActiveRoom;
    
    return <ul className="nav nav-pills nav-stacked">
      {_.map(rooms, function(info) {
       var cls = this.props.activeRoomId === info.id ? "active" : "";
       return <li className={cls} key={info.id} ><a onClick={() => this.props.setActiveRoom(info.id)}>{info.name}</a></li>
      }, this)}
      </ul>
    },
    propTypes: {
      rooms: React.PropTypes.arrayOf(React.PropTypes.shape({
        name: React.PropTypes.string,
        id: React.PropTypes.string
      })),
      setActiveRoom: React.PropTypes.func
  }
});

var Message = React.createClass({
  render: function() {
    return <dl className="dl-horizontal">
              <dt>{this.props.from}</dt>
              <dd>{this.props.text}</dd>
           </dl>
  }
});

var ChatWindow = React.createClass({
  getInitialState: function() {
    return { currentText: "" };
  },
  setText: function(e) {
    this.setState({currentText: e.target.value});
  },
  say: function(e) {
    e.preventDefault();
    this.props.say(this.state.currentText);
    this.setState({currentText: "" });
  },
  render: function() {    
    return <div>
        {_.map(this.props.messages, function(msg) {
          return <Message {...msg} />
        })}
                   
        <form onSubmit={this.say} className="form-inline">
          <input type="text" value={this.state.currentText} onChange={this.setText} />
          <input type="submit" value="Say" />
        </form>
      </div>
  }
});

var UserDetails = React.createClass({
  getInitialState: function() {
    return {
      username: this.props.initialUsername
    }    
  },
  setUsername: function(e) {
    this.setState({username: e.target.value});
  },
  notifyNewUsername: function(e) {
    e.preventDefault();
    this.props.setUsername(this.state.username);
  },
  revert: function() {
    this.setState({
      username: this.props.initialUsername
    });
  },
  render: function() {
    return <form onSubmit={this.notifyNewUsername} className="form-inline">
      <div className="form-group">
        <label htmlFor="username">Username:</label>
        <input className="form-control" id="username" type="text" value={this.state.username} onChange={this.setUsername} />
      </div>        
      <button type="submit" className="btn btn-default">Save</button>        
      <button type="submit" className="btn btn-default" onClick={this.revert}>Revert</button>
      </form>

  }
});

var _App = React.createClass({
  getInitialState: function() {
    return this.select(store.getState());
  },
  setUsername: function(username) {
    store.dispatch(actions.setUsername(username));
  },
  componentDidMount: function() {
    this.unsubscribe = store.subscribe(this.storeDataChanged);
  },
  select: function(state) {
    return {
      username: state.username,
      rooms: _.map(Object.keys(state.rooms.all), function(room_id) {
        return { id: room_id, name: state.rooms.all[room_id].name };
      }),
      messages: state.messages.all,
      activeRoomId: state.messages.activeRoomId,
    }
  },
  storeDataChanged: function() {
    this.setState(this.select(store.getState()));
  },
  componentWillUnmount: function() {
    this.unsubscribe();
  },
  setActiveRoom: function(room_id) {
    store.dispatch(actions.setActiveRoom(room_id));
  },
  say: function(text) {
    store.dispatch(actions.say(text));
  },
  render: function() {
    return <div className="container">
      <div className="row">
        <div className="col-xs-12">
          <UserDetails initialUsername={this.state.username} setUsername={this.setUsername} />
            </div>          
        </div>        
        <div className="row">
          <div className="col-xs-4">
            <RoomList rooms={this.state.rooms} setActiveRoom={this.setActiveRoom} activeRoomId={this.state.activeRoomId} />
          </div>
          <div className="col-xs-8">
            { this.state.activeRoomId ? 
             <ChatWindow messages={this.state.messages} say={this.say} /> :
             false
            }
          </div>
        </div>
      </div>
  }
});


React.render(<_App />, document.querySelector('main'));






View Compiled

External CSS

  1. //maxcdn.bootstrapcdn.com/bootstrap/3.3.5/css/bootstrap.min.css

External JavaScript

  1. https://cdnjs.cloudflare.com/ajax/libs/react/0.13.3/react-with-addons.js
  2. https://cdnjs.cloudflare.com/ajax/libs/redux/1.0.1/redux.min.js
  3. https://cdn.firebase.com/js/client/2.2.9/firebase.js
  4. https://cdnjs.cloudflare.com/ajax/libs/underscore.js/1.8.3/underscore-min.js
  5. https://cdnjs.cloudflare.com/ajax/libs/babel-core/5.6.15/browser-polyfill.min.js