diff --git a/package.json b/package.json index 15a2e25..c5db1ea 100644 --- a/package.json +++ b/package.json @@ -25,7 +25,10 @@ "react-redux": "4.4.0", "redux": "3.3.1", "redux-form": "~4.2.0", - "redux-thunk": "~2.0.1" + "redux-thunk": "~2.0.1", + "redux-logger": "~2.6.1", + "redux-promise": "~0.5.1", + "immutable": "~3.7.6" }, "devDependencies": { "babel-cli": "6.5.1", diff --git a/src/actions/RoomActionsFactory.js b/src/actions/RoomActionsFactory.js new file mode 100644 index 0000000..82b2097 --- /dev/null +++ b/src/actions/RoomActionsFactory.js @@ -0,0 +1,21 @@ +import { + ROOM_SELECT, + ROOM_JOIN +} from "../constants/ActionTypes"; +import ControlRequest from "../utils/ControlRequest"; + +export default (socketActions) => ({ + getRoomList: () => { + return socketActions.send(ControlRequest.roomList()); + }, + + select: (room) => ({ + type: ROOM_SELECT, + payload: room + }), + + join: (room) => ({ + type: ROOM_JOIN, + paylod: room + }) +}); diff --git a/src/actions/socketActionsFactory.js b/src/actions/SocketActionsFactory.js similarity index 100% rename from src/actions/socketActionsFactory.js rename to src/actions/SocketActionsFactory.js diff --git a/src/actions/socketHandlerActions.js b/src/actions/SocketHandlerActions.js similarity index 100% rename from src/actions/socketHandlerActions.js rename to src/actions/SocketHandlerActions.js diff --git a/src/actions/roomActions.js b/src/actions/roomActions.js deleted file mode 100644 index ee9a2d6..0000000 --- a/src/actions/roomActions.js +++ /dev/null @@ -1,8 +0,0 @@ -import { ROOM_SELECT } from "../constants/ActionTypes"; - -export default { - select: (room_name) => ({ - type: ROOM_SELECT, - payload: room_name - }) -}; diff --git a/src/components/ConnectForm.js b/src/components/ConnectForm.js index e8952a4..3b5d970 100644 --- a/src/components/ConnectForm.js +++ b/src/components/ConnectForm.js @@ -1,6 +1,7 @@ import React, {PropTypes} from "react"; import {reduxForm} from "redux-form"; +import SocketStatusPane from "./SocketStatusPane"; import { STATE_CLOSED } from "../constants/socket"; import ControlRequest from "../utils/ControlRequest"; @@ -10,13 +11,17 @@ const ConnectForm = (props) => { const onSubmit = handleSubmit((values) => socketOpen(values.url)); return ( -
- - -
+
+

Connect to a solstice client

+
+ + +
+ +
); }; diff --git a/src/components/Footer.js b/src/components/Footer.js deleted file mode 100644 index 65a431b..0000000 --- a/src/components/Footer.js +++ /dev/null @@ -1,21 +0,0 @@ -import React, { PropTypes } from "react"; - -import SocketStatusPane from "./SocketStatusPane"; - -import LoginStatusPane from "../containers/LoginStatusPane"; - -const Footer = ({ socket, socketActions }) => { - return ( - - ); -}; - -Footer.propTypes = { - socket: PropTypes.object.isRequired, - socketActions: PropTypes.object.isRequired -}; - -export default Footer; diff --git a/src/containers/LoginStatusPane.js b/src/components/LoginStatusPane.js similarity index 96% rename from src/containers/LoginStatusPane.js rename to src/components/LoginStatusPane.js index 6d8c415..a580d86 100644 --- a/src/containers/LoginStatusPane.js +++ b/src/components/LoginStatusPane.js @@ -88,6 +88,4 @@ LoginStatusPane.propTypes = { socketSend: PropTypes.func.isRequired }; -export default connect( - state => state.login -)(LoginStatusPane); +export default LoginStatusPane; diff --git a/src/components/Room.js b/src/components/Room.js index a3571b2..0184619 100644 --- a/src/components/Room.js +++ b/src/components/Room.js @@ -1,14 +1,21 @@ import React, { PropTypes } from "react"; -const Room = ({ name, onClick }) => { +const Room = ({ isSelected, name, onClick }) => { + let className; + if (isSelected) { + className = "room room-selected"; + } else { + className = "room"; + } return ( - + {name} ); }; Room.propTypes = { + isSelected: PropTypes.bool.isRequired, name: PropTypes.string.isRequired, onClick: PropTypes.func.isRequired }; diff --git a/src/components/RoomList.js b/src/components/RoomList.js index c9551b4..65ca86a 100644 --- a/src/components/RoomList.js +++ b/src/components/RoomList.js @@ -3,33 +3,50 @@ import React, { PropTypes } from "react"; import Room from "./Room"; import RoomListHeader from "./RoomListHeader"; -const RoomList = ({ refresh, rooms, roomActions }) => { - const children = []; - - for (const [room_name, room_data] of rooms) { - const onClick = (event) => { - roomActions.select(room_name); - }; - - children.push( -
  • - -
  • - ); +class RoomList extends React.Component { + constructor(props) { + super(props); } - return ( -
    - - -
    - ); -}; + componentDidMount() { + this.props.roomActions.getRoomList(); + } + + render() { + const { selected, rooms, roomActions } = this.props; + + const children = []; + + console.log(`Selected: "${selected}"`); + for (const [room_name, room_data] of rooms) { + const onClick = (event) => { + roomActions.select(room_name); + if (!room_data.joined) { + roomActions.join(room_name); + } + }; + + children.push( +
  • + +
  • + ); + } + + return ( +
    + + +
    + ); + } +} RoomList.propTypes = { - refresh: PropTypes.func.isRequired, rooms: PropTypes.object.isRequired, - roomActions: PropTypes.object.isRequired + roomActions: PropTypes.object.isRequired, + selected: PropTypes.string }; export default RoomList; diff --git a/src/components/SolsticeApp.js b/src/components/SolsticeApp.js index 0ac246b..66401c2 100644 --- a/src/components/SolsticeApp.js +++ b/src/components/SolsticeApp.js @@ -2,32 +2,31 @@ import React, {PropTypes} from "react"; import ConnectForm from "./ConnectForm"; import Header from "./Header"; -import Footer from "./Footer"; -import SocketStatusPane from "./SocketStatusPane"; -import LoginStatusPane from "../containers/LoginStatusPane"; import RoomsPane from "../containers/RoomsPane"; +import Footer from "../containers/Footer"; import { STATE_OPEN } from "../constants/socket"; +const ID = "solstice-app"; + const SolsticeApp = (props) => { const { actions, socket } = props; if (socket.state !== STATE_OPEN ) { return ( -
    +
    - -
    + socketOpen={actions.socket.open}/> + ); } return ( -
    +
    - +
    -
    +
    ); }; diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js index b57911c..5d86977 100644 --- a/src/constants/ActionTypes.js +++ b/src/constants/ActionTypes.js @@ -1,11 +1,12 @@ // Socket actions -export const SOCKET_SET_OPEN = Symbol("SOCKET_SET_OPEN"); -export const SOCKET_SET_OPENING = Symbol("SOCKET_SET_OPENING"); -export const SOCKET_SET_CLOSED = Symbol("SOCKET_SET_CLOSED"); -export const SOCKET_SET_CLOSING = Symbol("SOCKET_SET_CLOSING"); -export const SOCKET_SET_ERROR = Symbol("SOCKET_SET_ERROR"); +export const SOCKET_SET_OPEN = Symbol("SOCKET_SET_OPEN"); +export const SOCKET_SET_OPENING = Symbol("SOCKET_SET_OPENING"); +export const SOCKET_SET_CLOSED = Symbol("SOCKET_SET_CLOSED"); +export const SOCKET_SET_CLOSING = Symbol("SOCKET_SET_CLOSING"); +export const SOCKET_SET_ERROR = Symbol("SOCKET_SET_ERROR"); export const SOCKET_RECEIVE_MESSAGE = Symbol("SOCKET_RECEIVE_MESSAGE"); -export const SOCKET_SEND_MESSAGE = Symbol("SOCKET_SEND_MESSAGE"); +export const SOCKET_SEND_MESSAGE = Symbol("SOCKET_SEND_MESSAGE"); // Room actions export const ROOM_SELECT = Symbol("ROOM_SELECT"); +export const ROOM_JOIN = Symbol("ROOM_JOIN"); diff --git a/src/containers/App.js b/src/containers/App.js index 9c25550..3a31357 100644 --- a/src/containers/App.js +++ b/src/containers/App.js @@ -4,9 +4,12 @@ import React, {PropTypes} from "react"; import { bindActionCreators } from "redux"; import { connect } from "react-redux"; +import SocketActionsFactory from "../actions/SocketActionsFactory"; +import SocketHandlerActions from "../actions/SocketHandlerActions"; +import RoomActionsFactory from "../actions/RoomActionsFactory"; + import SolsticeApp from "../components/SolsticeApp"; -import socketActionsFactory from "../actions/socketActionsFactory"; -import socketHandlerActions from "../actions/socketHandlerActions"; + import SocketClient from "../utils/SocketClient"; const App = (props) => (); @@ -16,19 +19,17 @@ App.propTypes = { socket: PropTypes.object.isRequired }; -function mapStateToProps(state) { - return { - socket: state.socket - }; -} +const mapStateToProps = ({ socket }) => ({ socket }); function mapDispatchToProps(dispatch) { - const callbacks = bindActionCreators(socketHandlerActions, dispatch); + const callbacks = bindActionCreators(SocketHandlerActions, dispatch); const socketClient = new SocketClient(callbacks); - const socketActions = socketActionsFactory(socketClient); + const socketActions = SocketActionsFactory(socketClient); + const roomActions = RoomActionsFactory(socketActions); return { actions: { - socketActions: bindActionCreators(socketActions, dispatch) + room: bindActionCreators(roomActions, dispatch), + socket: bindActionCreators(socketActions, dispatch) } }; } diff --git a/src/containers/Footer.js b/src/containers/Footer.js new file mode 100644 index 0000000..664d389 --- /dev/null +++ b/src/containers/Footer.js @@ -0,0 +1,26 @@ +import React, { PropTypes } from "react"; +import { connect } from "react-redux"; + +import LoginStatusPane from "../components/LoginStatusPane"; +import SocketStatusPane from "../components/SocketStatusPane"; + +const Footer = ({ actions, login, socket }) => { + return ( +
    + + +
    + ); +}; + +Footer.propTypes = { + actions: PropTypes.object.isRequired, + login: PropTypes.object.isRequired, + socket: PropTypes.object.isRequired +}; + +const mapStateToProps = ({ socket, login }) => ({ socket, login }); + +export default connect( + mapStateToProps +)(Footer); diff --git a/src/containers/RoomChat.js b/src/containers/RoomChat.js index 2f6cccc..b1d0206 100644 --- a/src/containers/RoomChat.js +++ b/src/containers/RoomChat.js @@ -6,7 +6,7 @@ const RoomChat = ({ name }) => { }; RoomChat.propTypes = { - name: PropTypes.string.isRequired + name: PropTypes.string }; export default connect( diff --git a/src/containers/RoomsPane.js b/src/containers/RoomsPane.js index bfff689..ab1fee1 100644 --- a/src/containers/RoomsPane.js +++ b/src/containers/RoomsPane.js @@ -6,8 +6,6 @@ import RoomList from "../components/RoomList"; import RoomChat from "../containers/RoomChat"; -import roomActions from "../actions/roomActions"; - import ControlRequest from "../utils/ControlRequest"; class RoomsPane extends React.Component { @@ -15,40 +13,27 @@ class RoomsPane extends React.Component { super(props); } - componentDidMount() { - this.props.socketSend(ControlRequest.roomList()); - } - render() { - const refresh = () => { - this.props.socketSend(ControlRequest.roomList()); - }; - return (
    - + roomActions={this.props.actions.room} + selected={this.props.selected} + /> +
    ); } } RoomsPane.propTypes = { - rooms: PropTypes.object.isRequired, - roomActions: PropTypes.object.isRequired, - socketSend: PropTypes.func.isRequired + actions: PropTypes.object.isRequired, + rooms: PropTypes.object.isRequired }; const mapStateToProps = (state) => state.rooms; -const mapDispatchToProps = (dispatch) => ({ - roomActions: bindActionCreators(roomActions, dispatch) -}); - export default connect( mapStateToProps, - mapDispatchToProps )(RoomsPane); diff --git a/src/reducers/rooms.js b/src/reducers/rooms.js index afd3ed9..ade3b60 100644 --- a/src/reducers/rooms.js +++ b/src/reducers/rooms.js @@ -1,7 +1,13 @@ -import { ROOM_SELECT, SOCKET_RECEIVE_MESSAGE } from "../constants/ActionTypes"; +import Immutable from "immutable"; + +import { + ROOM_JOIN, + ROOM_SELECT, + SOCKET_RECEIVE_MESSAGE +} from "../constants/ActionTypes"; const initialState = { - rooms: new Map(), + rooms: Immutable.OrderedMap(), selected: null }; @@ -19,13 +25,16 @@ const reduceRoomList = (old_rooms, room_list) => { }); // Then build the new rooms map - const new_rooms = new Map(); + let new_rooms = Immutable.OrderedMap(); for (const [ room_name, room_data ] of room_list) { const old_data = old_rooms.get(room_name); if (old_data) { - new_rooms.set(room_name, { ...old_data, ...room_data }); + new_rooms = new_rooms.set(room_name, { + ...old_data, + ...room_data + }); } else { - new_rooms.set(room_name, room_data); + new_rooms = new_rooms.set(room_name, room_data); } } return new_rooms; @@ -34,10 +43,11 @@ const reduceRoomList = (old_rooms, room_list) => { const reduceReceiveMessage = (state, payload) => { switch (payload.variant) { case "RoomListResponse": - { - const rooms = reduceRoomList(state.rooms, payload.data.rooms); - return { ...state, rooms }; - } + return { + ...state, + rooms: reduceRoomList(state.rooms, payload.data.rooms) + }; + default: return state; } @@ -50,7 +60,23 @@ export default (state = initialState, action) => { return reduceReceiveMessage(state, payload); case ROOM_SELECT: - return { ...state, selected: payload }; + return { + ...state, + selected: payload + }; + + case ROOM_JOIN: + { + const rooms = state.rooms.merge({ + [payload]: { + joined: true + } + }); + return { + ...state, + rooms + }; + } default: return state; diff --git a/src/store/configureStore.js b/src/store/configureStore.js index 540c6f1..ecde3a1 100644 --- a/src/store/configureStore.js +++ b/src/store/configureStore.js @@ -1,5 +1,7 @@ import { applyMiddleware } from "redux"; import thunk from "redux-thunk"; +import promise from "redux-promise"; +import createLogger from "redux-logger"; let configureStore; if (process.env.NODE_ENV === 'production') { @@ -8,4 +10,10 @@ if (process.env.NODE_ENV === 'production') { configureStore = require('./configureStore.dev').default; } -export default () => configureStore(undefined, applyMiddleware(thunk)); +export default () => { + const logger = createLogger(); + return configureStore( + undefined, + applyMiddleware(thunk, promise, logger) + ); +}; diff --git a/src/styles/styles.scss b/src/styles/styles.scss index 531deae..806f87f 100644 --- a/src/styles/styles.scss +++ b/src/styles/styles.scss @@ -26,6 +26,8 @@ html { #solstice-app { display: flex; flex-flow: column; + justify-content: center; + align-items: center; } header { @@ -42,6 +44,7 @@ footer { } main { + width: 100%; height: 85%; margin: 0; padding: 0; @@ -64,7 +67,6 @@ main { flex-flow: column; border: solid grey 0.1em; height: 100%; - overflow: auto; } #room-chat { @@ -72,23 +74,27 @@ main { } #room-list-header { + flex: 1; display: flex; flex-flow: row; border: solid grey 0.1em; } -#room-list-header div { +#room-list-header > div { flex: 1; border: solid grey 0.1em; } #room-list ul { + display: block; list-style: none; padding: 0; margin: 0; + height: 84%; + overflow-y: auto; } -.room { +a.room { display: flex; color: inherit; text-decoration: inherit; @@ -96,14 +102,24 @@ main { border: solid grey 0.1em; } -.room:hover { +a.room:hover { background: lightgrey; } -.room:active { +a.room:active { background: grey; } +a.room-selected { + background: orange; +} + #login-status-pane, #socket-status-pane { flex: 1; } + +#connect-form { + display: flex; + flex-flow: column; + align-items: center; +} diff --git a/src/utils/ControlRequest.js b/src/utils/ControlRequest.js index d377fe7..d697a01 100644 --- a/src/utils/ControlRequest.js +++ b/src/utils/ControlRequest.js @@ -7,5 +7,10 @@ export default { roomList: () => ({ "variant": "RoomListRequest", "fields": [] + }), + + joinRoom: (room) => ({ + "variant": "JoinRoomRequest", + "fields": [{ room }] }) };