Browse Source

Huge move to react-router.

pull/1/head
Titouan Rigoudy 9 years ago
parent
commit
6df7f6864d
13 changed files with 262 additions and 217 deletions
  1. +37
    -68
      src/components/LoginStatusPane.js
  2. +11
    -7
      src/components/Room.js
  3. +12
    -7
      src/components/RoomChat.js
  4. +3
    -3
      src/components/RoomChatMessageList.js
  5. +0
    -1
      src/components/RoomList.js
  6. +10
    -40
      src/components/SolsticeApp.js
  7. +0
    -38
      src/containers/App.js
  8. +95
    -0
      src/containers/ConnectPage.js
  9. +2
    -3
      src/containers/Footer.js
  10. +36
    -24
      src/containers/RoomsPane.js
  11. +33
    -0
      src/createRoutes.js
  12. +8
    -2
      src/index.js
  13. +15
    -24
      src/reducers/rooms.js

+ 37
- 68
src/components/LoginStatusPane.js View File

@ -10,97 +10,66 @@ import {
LOGIN_STATUS_FAILURE
} from "../constants/login";
const ID = "login-status-pane";
class LoginStatusPane extends React.Component
{
constructor(props) {
super(props);
}
componentDidMount() {
const { status, loginActions } = this.props;
if (status == LOGIN_STATUS_UNKNOWN) {
loginActions.getStatus();
}
}
render() {
let statusText;
let motd;
let reason;
render_unknown() {
return (
<div id={ID}>
Login status: unknown
</div>
);
}
switch (this.props.status) {
case LOGIN_STATUS_UNKNOWN:
statusText = "unknown";
break;
render_getting() {
return (
<div id={ID}>
Login status: fetching...
</div>
);
}
case LOGIN_STATUS_GETTING:
statusText = "fetching";
break;
render_pending() {
return (
<div id={ID}>
Logging in as {this.props.username}...
</div>
);
}
case LOGIN_STATUS_PENDING:
statusText = `logging in as ${this.props.username}`;
break;
render_success() {
let motd_element;
if (this.props.motd) {
motd_element = (
<span id="login-motd">
MOTD: {this.props.motd}
</span>
);
case LOGIN_STATUS_SUCCESS:
statusText = `logged in as ${this.props.username}`;
motd = (
<span id="login-status-motd">
MOTD: {this.props.motd}
</span>
);
break;
case LOGIN_STATUS_FAILURE:
statusText = `failed to log in as ${this.props.username}`;
reason = (
<span id="login-status-reason">
Reason: {this.props.reason}
</span>
);
break;
}
return (
<div id={ID}>
Logged in as {this.props.username}
{motd_element}
</div>
);
}
render_failure() {
return (
<div id={ID}>
Failed to log in as {this.props.username}
<span id="login-reason">
Reason: {this.props.reason}
<div id="login-status-pane">
<span id="login-status-text">
Login status: {statusText}
</span>
{motd}
{reason}
</div>
);
}
render() {
switch (this.props.status) {
case LOGIN_STATUS_UNKNOWN:
return this.render_unknown();
case LOGIN_STATUS_GETTING:
return this.render_getting();
case LOGIN_STATUS_PENDING:
return this.render_pending();
case LOGIN_STATUS_SUCCESS:
return this.render_success();
case LOGIN_STATUS_FAILURE:
return this.render_failure();
}
}
}
LoginStatusPane.propTypes = {
status: propTypeSymbol.isRequired,
username: PropTypes.string,
motd: PropTypes.string,
reason: PropTypes.string,
loginActions: PropTypes.shape({
getStatus: PropTypes.func.isRequired
}).isRequired
reason: PropTypes.string
};
export default LoginStatusPane;

+ 11
- 7
src/components/Room.js View File

@ -1,23 +1,27 @@
import React, { PropTypes } from "react";
import { Link } from "react-router";
const Room = ({ isSelected, name, onClick, room }) => {
const Room = ({ name, onClick, room }) => {
const classes = ["room"];
if (isSelected) {
classes.push("room-selected");
}
if (room.membership == "Member") {
classes.push("room-joined");
}
const base64Name = btoa(encodeURIComponent(name));
const path = `/app/rooms/${base64Name}`;
return (
<a className={classes.join(" ")} onClick={onClick} href="#">
<Link to={path}
activeClassName="room-selected"
className={classes.join(" ")}
>
<span className="room-name">{name}</span>
<span className="room-user-count">({room.user_count})</span>
</a>
</Link>
);
};
Room.propTypes = {
isSelected: PropTypes.bool.isRequired,
name: PropTypes.string.isRequired,
onClick: PropTypes.func.isRequired,
room: PropTypes.shape({


+ 12
- 7
src/components/RoomChat.js View File

@ -1,9 +1,13 @@
import React, { PropTypes } from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import ImmutablePropTypes from "react-immutable-proptypes";
import RoomChatForm from "./RoomChatForm";
import RoomChatHeader from "./RoomChatHeader";
import RoomChatMessageList from "./RoomChatMessageList";
import RoomActions from "../actions/RoomActions";
import RoomChatForm from "../components/RoomChatForm";
import RoomChatHeader from "../components/RoomChatHeader";
import RoomChatMessageList from "../components/RoomChatMessageList";
const ID = "room-chat";
@ -28,7 +32,7 @@ class RoomChat extends React.Component {
}
render() {
const { login_user_name, room, roomActions } = this.props;
const { loginUserName, room, roomActions } = this.props;
if (!room) {
return (
@ -66,7 +70,7 @@ class RoomChat extends React.Component {
<div id={ID}>
{header}
<RoomChatMessageList
login_user_name={login_user_name}
loginUserName={loginUserName}
messages={messages}
/>
<RoomChatForm
@ -79,11 +83,12 @@ class RoomChat extends React.Component {
}
RoomChat.propTypes = {
login_user_name: PropTypes.string,
loginUserName: PropTypes.string,
room: PropTypes.shape({
name: PropTypes.string.isRequired,
membership: PropTypes.string.isRequired,
messages: ImmutablePropTypes.list.isRequired
messages: ImmutablePropTypes.list.isRequired,
showUsers: PropTypes.bool
}),
roomActions: PropTypes.shape({
join: PropTypes.func.isRequired,


+ 3
- 3
src/components/RoomChatMessageList.js View File

@ -1,12 +1,12 @@
import React, { PropTypes } from "react";
import ImmutablePropTypes from "react-immutable-proptypes";
const RoomChatMessageList = ({ login_user_name, messages }) => {
const RoomChatMessageList = ({ loginUserName, messages }) => {
// Append all messages in the chat room.
const children = [];
let i = 0;
for (const { user_name, message } of messages) {
if (user_name == login_user_name) {
if (user_name == loginUserName) {
children.push(
<li key={i} className="room-chat-message room-chat-message-me">
<div className="room-chat-message-text">{message}</div>
@ -27,7 +27,7 @@ const RoomChatMessageList = ({ login_user_name, messages }) => {
};
RoomChatMessageList.propTypes = {
login_user_name: PropTypes.string.isRequired,
loginUserName: PropTypes.string.isRequired,
messages: ImmutablePropTypes.listOf(
PropTypes.shape({
user_name: PropTypes.string.isRequired,


+ 0
- 1
src/components/RoomList.js View File

@ -28,7 +28,6 @@ class RoomList extends React.Component {
onClick={onClick}
name={room_name}
room={room_data}
isSelected={selected == room_name}
/>
</li>
);


+ 10
- 40
src/components/SolsticeApp.js View File

@ -1,51 +1,21 @@
import React, {PropTypes} from "react";
import { STATE_OPEN } from "../constants/socket";
import { LOGIN_STATUS_SUCCESS } from "../constants/login";
import ConnectForm from "./ConnectForm";
import Header from "./Header";
import LoginStatusPane from "./LoginStatusPane";
import Footer from "../containers/Footer";
import RoomsPane from "../containers/RoomsPane";
const ID = "solstice-app";
const SolsticeApp = (props) => {
const { actions, login, socket } = props;
if (socket.state !== STATE_OPEN ) {
return (
<div id={ID}>
<ConnectForm socket={socket} actions={actions} />
</div>
);
}
if (login.status !== LOGIN_STATUS_SUCCESS) {
return (
<div id={ID}>
<ConnectForm socket={socket} actions={actions} />
<LoginStatusPane {...login} loginActions={actions.login} />
</div>
);
}
return (
<div id={ID}>
<Header />
<main>
<RoomsPane roomActions={actions.room}/>
</main>
<Footer actions={actions} />
</div>
);
};
const SolsticeApp = ({ children }) => (
<div id="solstice-app">
<Header />
<main>
{children}
</main>
<Footer />
</div>
);
SolsticeApp.propTypes = {
actions: PropTypes.object.isRequired,
login: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired
children: PropTypes.element
};
export default SolsticeApp;

+ 0
- 38
src/containers/App.js View File

@ -1,38 +0,0 @@
// This file bootstraps the app with the boilerplate necessary
// to support hot reloading in Redux
import React, {PropTypes} from "react";
import { bindActionCreators } from "redux";
import { connect } from "react-redux";
import LoginActions from "../actions/LoginActions";
import RoomActions from "../actions/RoomActions";
import SocketActions from "../actions/SocketActions";
import SocketHandlerActions from "../actions/SocketHandlerActions";
import SolsticeApp from "../components/SolsticeApp";
const App = (props) => (<SolsticeApp {...props} />);
App.propTypes = {
actions: PropTypes.object.isRequired,
login: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired
};
const mapStateToProps = ({ socket, login }) => ({ socket, login });
function mapDispatchToProps(dispatch) {
return {
actions: {
login: bindActionCreators(LoginActions, dispatch),
room: bindActionCreators(RoomActions, dispatch),
socket: bindActionCreators(SocketActions, dispatch),
socketHandlers: bindActionCreators(SocketHandlerActions, dispatch)
}
};
}
export default connect(
mapStateToProps,
mapDispatchToProps
)(App);

+ 95
- 0
src/containers/ConnectPage.js View File

@ -0,0 +1,95 @@
import React, { PropTypes } from "react";
import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import { hashHistory, withRouter } from "react-router";
import LoginActions from "../actions/LoginActions";
import SocketActions from "../actions/SocketActions";
import SocketHandlerActions from "../actions/SocketHandlerActions";
import { STATE_OPEN } from "../constants/socket";
import {
LOGIN_STATUS_SUCCESS,
LOGIN_STATUS_UNKNOWN
} from "../constants/login";
import ConnectForm from "../components/ConnectForm";
import LoginStatusPane from "../components/LoginStatusPane";
class ConnectPage extends React.Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.getLoginStatusOrRedirect(this.props);
}
componentWillReceiveProps(nextProps) {
this.getLoginStatusOrRedirect(nextProps);
}
getLoginStatusOrRedirect(props) {
const { actions, login, router, socket } = props;
if (socket.state === STATE_OPEN)
{
switch (login.status) {
case LOGIN_STATUS_UNKNOWN:
actions.login.getStatus();
break;
case LOGIN_STATUS_SUCCESS:
router.push("/app/rooms");
break;
}
}
}
render() {
const { actions, login, socket } = this.props;
let loginStatusPane;
if (socket.state === STATE_OPEN &&
login.status === LOGIN_STATUS_UNKNOWN)
{
loginStatusPane = (
<LoginStatusPane {...login} loginActions={actions.login} />
);
}
return (
<div id="connect-page">
<ConnectForm socket={socket} actions={actions} />
{loginStatusPane}
</div>
);
}
}
ConnectPage.propTypes = {
actions: PropTypes.shape({
login: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired,
socketHandlers: PropTypes.object.isRequired
}).isRequired,
login: PropTypes.object.isRequired,
router: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired
};
const mapStateToProps = ({ login, socket }) => ({ login, socket });
const mapDispatchToProps = (dispatch) => ({
actions: {
login: bindActionCreators(LoginActions, dispatch),
socket: bindActionCreators(SocketActions, dispatch),
socketHandlers: bindActionCreators(SocketHandlerActions, dispatch)
}
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(ConnectPage));

+ 2
- 3
src/containers/Footer.js View File

@ -4,17 +4,16 @@ import { connect } from "react-redux";
import LoginStatusPane from "../components/LoginStatusPane";
import SocketStatusPane from "../components/SocketStatusPane";
const Footer = ({ actions, login, socket }) => {
const Footer = ({ login, socket }) => {
return (
<footer>
<SocketStatusPane {...socket} />
<LoginStatusPane {...login} loginActions={actions.login} />
<LoginStatusPane {...login} />
</footer>
);
};
Footer.propTypes = {
actions: PropTypes.object.isRequired,
login: PropTypes.object.isRequired,
socket: PropTypes.object.isRequired
};


+ 36
- 24
src/containers/RoomsPane.js View File

@ -3,6 +3,8 @@ import { connect } from "react-redux";
import { bindActionCreators } from "redux";
import ImmutablePropTypes from "react-immutable-proptypes";
import RoomActions from "../actions/RoomActions";
import RoomChat from "../components/RoomChat";
import RoomList from "../components/RoomList";
import RoomUserList from "../components/RoomUserList";
@ -13,19 +15,28 @@ class RoomsPane extends React.Component {
}
render() {
const { login_user_name, rooms, roomActions, selected } = this.props;
const { loginUserName, params, rooms, roomActions } = this.props;
let room;
if (selected) {
room = {
...rooms.get(selected),
name: selected
};
}
let roomName;
let roomChat;
let roomUserList;
if (room && room.showUsers) {
roomUserList = <RoomUserList users={room.members} />;
if (params && params.roomName) {
roomName = decodeURIComponent(atob(params.roomName));
const { membership, messages, showUsers } = rooms.get(roomName);
const room = {
name: roomName,
membership,
messages,
showUsers
};
roomChat = (
<RoomChat
loginUserName={loginUserName}
room={room}
roomActions={roomActions}
/>
);
}
return (
@ -33,15 +44,10 @@ class RoomsPane extends React.Component {
<RoomList
rooms={rooms}
roomActions={roomActions}
selected={selected}
selected={roomName}
/>
<div id="room-selected-pane">
<RoomChat
login_user_name={login_user_name}
room={room}
roomActions={roomActions}
/>
{roomUserList}
{roomChat}
</div>
</div>
);
@ -49,18 +55,24 @@ class RoomsPane extends React.Component {
}
RoomsPane.propTypes = {
login_user_name: PropTypes.string,
rooms: ImmutablePropTypes.orderedMap.isRequired,
roomActions: PropTypes.object.isRequired,
selected: PropTypes.string
loginUserName: PropTypes.string.isRequired,
params: PropTypes.shape({
roomName: PropTypes.string
}),
rooms: ImmutablePropTypes.orderedMap.isRequired,
roomActions: PropTypes.object.isRequired
};
const mapStateToProps = (state) => ({
login_user_name: state.login.username,
rooms: state.rooms.rooms,
selected: state.rooms.selected
loginUserName: state.login.username
});
const mapDispatchToProps = (dispatch) => ({
roomActions: bindActionCreators(RoomActions, dispatch)
});
export default connect(
mapStateToProps,
mapDispatchToProps
)(RoomsPane);

+ 33
- 0
src/createRoutes.js View File

@ -0,0 +1,33 @@
import React, { PropTypes } from 'react';
import { Route, IndexRoute } from 'react-router';
import ConnectPage from "./containers/ConnectPage";
import RoomsPane from "./containers/RoomsPane";
import SolsticeApp from "./components/SolsticeApp";
import { STATE_OPEN } from "./constants/socket";
import { LOGIN_STATUS_SUCCESS } from "./constants/login";
const createRoutes = (store) => {
const requireLoggedIn = (nextState, replaceState) => {
let { socket, login } = store.getState();
if (socket.state !== STATE_OPEN ||
login.status !== LOGIN_STATUS_SUCCESS)
{
replaceState({}, "/");
}
};
return (
<Route path="/">
<IndexRoute component={ConnectPage} />
<Route path="app" onEnter={requireLoggedIn} component={SolsticeApp}>
<Route path="rooms(/:roomName)" component={RoomsPane} />
</Route>
</Route>
);
};
export default createRoutes;

+ 8
- 2
src/index.js View File

@ -1,14 +1,20 @@
import React from 'react';
import {render} from 'react-dom';
import { Provider } from 'react-redux';
import App from './containers/App';
import { Router, hashHistory } from "react-router";
import configureStore from './store/configureStore';
import createRoutes from "./createRoutes";
import './styles/styles.scss'; //Yep, that's right. You can import SASS/CSS files too! Webpack will run the associated loader and plug this into the page.
const store = configureStore();
const routes = createRoutes(store);
render(
<Provider store={store}>
<App />
<Router history={hashHistory}>
{routes}
</Router>
</Provider>, document.getElementById('app')
);

+ 15
- 24
src/reducers/rooms.js View File

@ -4,46 +4,43 @@ import {
ROOM_JOIN,
ROOM_LEAVE,
ROOM_MESSAGE,
ROOM_SELECT,
ROOM_SHOW_USERS,
ROOM_HIDE_USERS,
SOCKET_RECEIVE_MESSAGE
} from "../constants/ActionTypes";
const initialState = {
rooms: Immutable.OrderedMap(),
selected: null
rooms: Immutable.OrderedMap()
};
const reduceRoomList = (old_rooms, room_list) => {
const reduceRoomList = (oldRoomMap, roomList) => {
// First sort the room list by room name
room_list.sort((room_pair_1, room_pair_2) => {
const name_1 = room_pair_1[0];
const name_2 = room_pair_2[0];
if (name_1 < name_2) {
roomList.sort(([ roomName1 ], [ roomName2 ]) => {
if (roomName1 < roomName2) {
return -1;
} else if (name_1 > name_2) {
} else if (roomName1 > roomName2) {
return 1;
}
return 0;
});
// Then build the new rooms map
let new_rooms = Immutable.OrderedMap();
for (const [ room_name, room_data ] of room_list) {
let newRoomMap = Immutable.OrderedMap();
for (const [ roomName, newRoomData ] of roomList) {
// Transform room_data.messages to an immutable list.
room_data.messages = Immutable.List(room_data.messages);
newRoomData.messages = Immutable.List(newRoomData.messages);
// Get the old room data.
const old_data = old_rooms.get(room_name);
const oldRoomData = oldRoomMap.get(roomName);
// Merge the old data and the new data, overwriting with new data if
// conflicting.
const new_data = {
...old_data,
...room_data
const mergedRoomData = {
...oldRoomData,
...newRoomData
};
new_rooms = new_rooms.set(room_name, new_data);
newRoomMap = newRoomMap.set(roomName, mergedRoomData);
}
return new_rooms;
return newRoomMap;
};
const reduceReceiveMessage = (rooms, { variant, data }) => {
@ -103,12 +100,6 @@ export default (state = initialState, action) => {
case ROOM_MESSAGE:
return state;
case ROOM_SELECT:
return {
...state,
selected: payload
};
case ROOM_JOIN:
{
const rooms = state.rooms.set(payload, {


Loading…
Cancel
Save