| @ -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; | |||||