| @ -1,18 +0,0 @@ | |||
| import React from "react"; | |||
| import { Link } from "react-router-dom"; | |||
| const Header = () => { | |||
| return ( | |||
| <header> | |||
| <h1>Solstice web UI</h1> | |||
| <Link to="/app/rooms" activeClassName="active"> | |||
| Rooms | |||
| </Link> | |||
| <Link to="/app/users" activeClassName="active"> | |||
| Users | |||
| </Link> | |||
| </header> | |||
| ); | |||
| }; | |||
| export default Header; | |||
| @ -0,0 +1,16 @@ | |||
| import { FC } from "react"; | |||
| import { Link } from "react-router-dom"; | |||
| const Header: FC = () => ( | |||
| <header> | |||
| <h1>Solstice web UI</h1> | |||
| <Link to="/rooms" activeClassName="active"> | |||
| Rooms | |||
| </Link> | |||
| <Link to="/users" activeClassName="active"> | |||
| Users | |||
| </Link> | |||
| </header> | |||
| ); | |||
| export default Header; | |||
| @ -1,24 +0,0 @@ | |||
| import React, { PropTypes } from "react"; | |||
| import ImmutablePropTypes from "react-immutable-proptypes"; | |||
| import Room from "./Room"; | |||
| import SearchableList from "./SearchableList"; | |||
| const ComposedSearchableList = SearchableList(Room); | |||
| const RoomList = ({ rooms, roomActions }) => ( | |||
| <ComposedSearchableList | |||
| id="room-list" | |||
| itemMap={rooms} | |||
| refresh={roomActions.getList} | |||
| /> | |||
| ); | |||
| RoomList.propTypes = { | |||
| rooms: ImmutablePropTypes.record.isRequired, | |||
| roomActions: PropTypes.shape({ | |||
| getList: PropTypes.func.isRequired, | |||
| }).isRequired, | |||
| }; | |||
| export default RoomList; | |||
| @ -0,0 +1,29 @@ | |||
| import { FC } from "react"; | |||
| import { useDispatch } from "react-redux"; | |||
| import RoomComponent from "./Room"; | |||
| import SearchableList from "./SearchableList"; | |||
| import { Room, RoomSliceState } from "../modules/room/slice"; | |||
| import { socketSendMessage } from "../modules/websocket/slice"; | |||
| const RoomList: FC<RoomSliceState> = ({ rooms }) => { | |||
| const dispatch = useDispatch(); | |||
| const refresh = () => { | |||
| dispatch( | |||
| socketSendMessage({ | |||
| variant: "RoomListRequest", | |||
| fields: [], | |||
| }) | |||
| ); | |||
| }; | |||
| return ( | |||
| <SearchableList<Room, RoomComponent> | |||
| id="room-list" | |||
| map={rooms} | |||
| refresh={refresh} | |||
| /> | |||
| ); | |||
| }; | |||
| export default RoomList; | |||
| @ -1,57 +0,0 @@ | |||
| import React, { PropTypes } from "react"; | |||
| import ImmutablePropTypes from "react-immutable-proptypes"; | |||
| const SearchableList = (ItemComponent) => { | |||
| class ComposedSearchableList extends React.Component { | |||
| constructor(props) { | |||
| super(props); | |||
| } | |||
| componentDidMount() { | |||
| const { itemMap, refresh } = this.props; | |||
| if (itemMap.shouldUpdate()) { | |||
| refresh(); | |||
| } | |||
| } | |||
| render() { | |||
| const { id, itemMap, refresh } = this.props; | |||
| const children = []; | |||
| for (const [itemName, itemData] of itemMap.byName) { | |||
| children.push( | |||
| <li key={itemName}> | |||
| <ItemComponent name={itemName} data={itemData} /> | |||
| </li> | |||
| ); | |||
| } | |||
| const onClick = (event) => { | |||
| event.preventDefault(); | |||
| refresh(); | |||
| }; | |||
| return ( | |||
| <div id={id}> | |||
| <div id={`${id}-header`}> | |||
| <div> | |||
| <button onClick={onClick}>Refresh</button> | |||
| </div> | |||
| </div> | |||
| <ul>{children}</ul> | |||
| </div> | |||
| ); | |||
| } | |||
| } | |||
| ComposedSearchableList.propTypes = { | |||
| id: PropTypes.string.isRequired, | |||
| itemMap: ImmutablePropTypes.record.isRequired, | |||
| refresh: PropTypes.func.isRequired, | |||
| }; | |||
| return ComposedSearchableList; | |||
| }; | |||
| export default SearchableList; | |||
| @ -0,0 +1,43 @@ | |||
| import { ComponentType, FC } from "react"; | |||
| interface Props<Item> { | |||
| id: string; | |||
| map: { [key: string]: Item }; | |||
| refresh: () => void; | |||
| } | |||
| function SearchableList< | |||
| Item, | |||
| ItemComponent extends ComponentType<Props<Item>> | |||
| >({ id, map, refresh }: Props<Item>) { | |||
| const children = []; | |||
| for (const name in map) { | |||
| children.push( | |||
| <li key={name}> | |||
| <ItemComponent name={name} data={map[name]} /> | |||
| </li> | |||
| ); | |||
| } | |||
| const onClick = (event) => { | |||
| event.preventDefault(); | |||
| refresh(); | |||
| }; | |||
| return ( | |||
| <div id={id}> | |||
| <div id={`${id}-header`}> | |||
| <div> | |||
| <button onClick={onClick}>Refresh</button> | |||
| </div> | |||
| </div> | |||
| <ul>{children}</ul> | |||
| </div> | |||
| ); | |||
| } | |||
| // eslint-disable-next-line | |||
| let _assertType: FC<Props<Item>> = SearchableList; | |||
| export default SearchableList; | |||
| @ -1,47 +0,0 @@ | |||
| import React from "react"; | |||
| import { connect } from "react-redux"; | |||
| import { Switch, Redirect, Route, useLocation } from "react-router-dom"; | |||
| import Header from "./Header"; | |||
| import { STATE_OPEN } from "../constants/socket"; | |||
| import ConnectPage from "../containers/ConnectPage"; | |||
| import Footer from "../containers/Footer"; | |||
| import { SocketRecord } from "../reducers/socket"; | |||
| const ConnectedApp: React.FC<SocketRecord> = ({ socket, children }) => { | |||
| const location = useLocation(); | |||
| if (socket.state !== STATE_OPEN) { | |||
| return ( | |||
| <Redirect | |||
| to={{ | |||
| pathname: "/connect", | |||
| state: { from: location }, | |||
| }} | |||
| /> | |||
| ); | |||
| } | |||
| return ( | |||
| <div id="solstice-app"> | |||
| <Header /> | |||
| <main>{children}</main> | |||
| <Footer /> | |||
| </div> | |||
| ); | |||
| }; | |||
| const SolsticeApp = ({ socket, children }) => ( | |||
| <Switch> | |||
| <Route path="/connect"> | |||
| <ConnectPage /> | |||
| </Route> | |||
| <Route path="/"> | |||
| <ConnectedApp socket={socket}>{children}</ConnectedApp> | |||
| </Route> | |||
| </Switch> | |||
| ); | |||
| const mapStateToProps = ({ socket }) => ({ socket }); | |||
| export default connect(mapStateToProps)(SolsticeApp); | |||
| @ -0,0 +1,67 @@ | |||
| import { FC } from "react"; | |||
| import { useSelector } from "react-redux"; | |||
| import { | |||
| Switch, | |||
| Redirect, | |||
| Route, | |||
| useLocation, | |||
| useRouteMatch, | |||
| } from "react-router-dom"; | |||
| import { selectSocket, SocketState } from "../modules/websocket/slice"; | |||
| import Header from "./Header"; | |||
| import ConnectPage from "../containers/ConnectPage"; | |||
| import Footer from "../containers/Footer"; | |||
| import RoomsPane from "../containers/RoomsPane"; | |||
| const MainPane: FC = () => { | |||
| const { path } = useRouteMatch(); | |||
| return ( | |||
| <main> | |||
| <Switch> | |||
| <Route path={`${path}rooms`}> | |||
| <RoomsPane /> | |||
| </Route> | |||
| <Route path={`${path}users`}>Coming soon: users pane.</Route> | |||
| </Switch> | |||
| </main> | |||
| ); | |||
| }; | |||
| const ConnectedApp: FC = ({ children }) => { | |||
| const socket = useSelector(selectSocket); | |||
| const location = useLocation(); | |||
| if (socket.state !== SocketState.Open) { | |||
| return ( | |||
| <Redirect | |||
| to={{ | |||
| pathname: "/connect", | |||
| state: { from: location }, | |||
| }} | |||
| /> | |||
| ); | |||
| } | |||
| return children; | |||
| }; | |||
| const SolsticeApp = () => ( | |||
| <Switch> | |||
| <Route path="/connect"> | |||
| <ConnectPage /> | |||
| </Route> | |||
| <Route path="/"> | |||
| <ConnectedApp> | |||
| <div id="solstice-app"> | |||
| <Header /> | |||
| <MainPane /> | |||
| <Footer /> | |||
| </div> | |||
| </ConnectedApp> | |||
| </Route> | |||
| </Switch> | |||
| ); | |||
| export default SolsticeApp; | |||
| @ -1,67 +0,0 @@ | |||
| import React, { PropTypes } from "react"; | |||
| 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"; | |||
| const RoomsPane = (props) => { | |||
| const { loginUserName, params, rooms, roomActions } = props; | |||
| let roomName; | |||
| let roomChat; | |||
| if (params && params.roomNameHash) { | |||
| roomName = rooms.getNameByHash(params.roomNameHash); | |||
| const roomData = rooms.getByName(roomName); | |||
| if (roomData) { | |||
| const room = { | |||
| name: roomName, | |||
| membership: roomData.membership, | |||
| messages: roomData.messages, | |||
| showUsers: roomData.showUsers, | |||
| }; | |||
| roomChat = ( | |||
| <RoomChat | |||
| loginUserName={loginUserName} | |||
| room={room} | |||
| roomActions={roomActions} | |||
| /> | |||
| ); | |||
| } | |||
| } | |||
| return ( | |||
| <div id="rooms-pane"> | |||
| <RoomList rooms={rooms} roomActions={roomActions} /> | |||
| <div id="room-selected-pane">{roomChat}</div> | |||
| </div> | |||
| ); | |||
| }; | |||
| RoomsPane.propTypes = { | |||
| loginUserName: PropTypes.string.isRequired, | |||
| params: PropTypes.shape({ | |||
| roomNameHash: PropTypes.string, | |||
| }), | |||
| rooms: ImmutablePropTypes.record.isRequired, | |||
| roomActions: PropTypes.object.isRequired, | |||
| }; | |||
| const mapStateToProps = (state) => ({ | |||
| loginUserName: state.login.username, | |||
| rooms: state.rooms, | |||
| }); | |||
| const mapDispatchToProps = (dispatch) => ({ | |||
| roomActions: bindActionCreators(RoomActions, dispatch), | |||
| }); | |||
| export default connect(mapStateToProps, mapDispatchToProps)(RoomsPane); | |||
| @ -0,0 +1,46 @@ | |||
| import { FC } from "react"; | |||
| import { useSelector } from "react-redux"; | |||
| //import RoomChat from "../components/RoomChat"; | |||
| import RoomList from "../components/RoomList"; | |||
| import { selectAllRooms } from "../modules/room/slice"; | |||
| const RoomsPane: FC = () => { | |||
| const rooms = useSelector(selectAllRooms); | |||
| let roomChat; | |||
| /* | |||
| if (params && params.roomNameHash) { | |||
| const roomName = rooms.getNameByHash(params.roomNameHash); | |||
| const roomData = rooms.getByName(roomName); | |||
| if (roomData) { | |||
| const room = { | |||
| name: roomName, | |||
| membership: roomData.membership, | |||
| messages: roomData.messages, | |||
| showUsers: roomData.showUsers, | |||
| }; | |||
| roomChat = ( | |||
| <RoomChat | |||
| loginUserName={loginUserName} | |||
| room={room} | |||
| roomActions={roomActions} | |||
| /> | |||
| ); | |||
| } | |||
| } | |||
| */ | |||
| return ( | |||
| <div id="rooms-pane"> | |||
| <RoomList rooms={rooms} /> | |||
| <div id="room-selected-pane">{roomChat}</div> | |||
| </div> | |||
| ); | |||
| }; | |||
| export default RoomsPane; | |||
| @ -0,0 +1,87 @@ | |||
| import { createSlice, PayloadAction } from "@reduxjs/toolkit"; | |||
| import { RootState } from "../../app/store"; | |||
| export enum RoomMembership { | |||
| Joining, | |||
| Joined, | |||
| Leaving, | |||
| Left, | |||
| } | |||
| export interface Room { | |||
| name: string; | |||
| membership: RoomMembership; | |||
| visibility: string; | |||
| operated: boolean; | |||
| userCount: number; | |||
| owner: string; | |||
| operators: Set<string>; | |||
| members: Set<string>; | |||
| messages: string[]; | |||
| tickers: string[]; | |||
| // showUsers: boolean; | |||
| } | |||
| export interface RoomSliceState { | |||
| [name: string]: Room; | |||
| } | |||
| const initialState: RoomSliceState = {}; | |||
| export interface RoomMessage { | |||
| room_name: string; | |||
| user_name: string; | |||
| message: string; | |||
| } | |||
| export const roomSlice = createSlice({ | |||
| name: "rooms", | |||
| initialState, | |||
| reducers: { | |||
| roomSetMembership: ( | |||
| state: RoomSliceState, | |||
| action: PayloadAction<[string, RoomMembership]> | |||
| ) => { | |||
| const [name, membership] = action.payload; | |||
| const room = state[name]; | |||
| if (room === undefined) { | |||
| console.log(`Cannot set membership of room ${name}`); | |||
| return; | |||
| } | |||
| room.membership = membership; | |||
| }, | |||
| roomMessage: ( | |||
| state: RoomSliceState, | |||
| action: PayloadAction<RoomMessage> | |||
| ) => { | |||
| const { room_name, user_name, message } = action.payload; | |||
| const room = state[room_name]; | |||
| if (room === undefined) { | |||
| console.log( | |||
| `Unknown room ${room_name} received message from ` + | |||
| `${user_name}: ${message}` | |||
| ); | |||
| return; | |||
| } | |||
| room.messages.push({ user_name, message }); | |||
| }, | |||
| roomSetAll: (state: RoomSliceState, action: PayloadAction<Room[]>) => { | |||
| state.rooms = {}; | |||
| for (const room of action.payload) { | |||
| state[room.name] = room; | |||
| } | |||
| }, | |||
| }, | |||
| }); | |||
| export const { roomSetMembership, roomMessage, roomSetAll } = roomSlice.actions; | |||
| export const selectAllRooms = (state: RootState) => state.rooms.rooms; | |||
| export const selectRoom = (name: string) => (state: RootState) => | |||
| state.rooms.rooms.get(name); | |||
| export default roomSlice.reducer; | |||