diff --git a/src/app/store.ts b/src/app/store.ts index f9fbd62..5eecac9 100644 --- a/src/app/store.ts +++ b/src/app/store.ts @@ -9,6 +9,7 @@ import counterReducer from "../features/counter/counterSlice"; // import { loginSocketMessageHandlers } from "../modules/login/message"; import loginReducer from "../modules/login/slice"; +import roomReducer from "../modules/room/slice"; import makeSocketMiddleware from "../modules/websocket/middleware"; import socketReducer from "../modules/websocket/slice"; @@ -16,7 +17,7 @@ export const store = configureStore({ reducer: { counter: counterReducer, login: loginReducer, - //rooms, + rooms: roomReducer, socket: socketReducer, //users, }, diff --git a/src/components/Header.js b/src/components/Header.js deleted file mode 100644 index 58883c0..0000000 --- a/src/components/Header.js +++ /dev/null @@ -1,18 +0,0 @@ -import React from "react"; -import { Link } from "react-router-dom"; - -const Header = () => { - return ( -
-

Solstice web UI

- - Rooms - - - Users - -
- ); -}; - -export default Header; diff --git a/src/components/Header.tsx b/src/components/Header.tsx new file mode 100644 index 0000000..ac7df07 --- /dev/null +++ b/src/components/Header.tsx @@ -0,0 +1,16 @@ +import { FC } from "react"; +import { Link } from "react-router-dom"; + +const Header: FC = () => ( +
+

Solstice web UI

+ + Rooms + + + Users + +
+); + +export default Header; diff --git a/src/components/Room.js b/src/components/Room.js index 680193e..dc77445 100644 --- a/src/components/Room.js +++ b/src/components/Room.js @@ -1,6 +1,5 @@ import React, { PropTypes } from "react"; -import { Link } from "react-router"; -import ImmutablePropTypes from "react-immutable-proptypes"; +import { Link } from "react-router-dom"; import md5 from "md5"; @@ -26,9 +25,4 @@ const Room = ({ name, data }) => { ); }; -Room.propTypes = { - name: PropTypes.string.isRequired, - data: ImmutablePropTypes.map.isRequired, -}; - export default Room; diff --git a/src/components/RoomChat.js b/src/components/RoomChat.js index 74da11a..3e6e731 100644 --- a/src/components/RoomChat.js +++ b/src/components/RoomChat.js @@ -1,7 +1,6 @@ import React, { PropTypes } from "react"; import { bindActionCreators } from "redux"; import { connect } from "react-redux"; -import ImmutablePropTypes from "react-immutable-proptypes"; import RoomActions from "../actions/RoomActions"; @@ -73,18 +72,4 @@ class RoomChat extends React.Component { } } -RoomChat.propTypes = { - loginUserName: PropTypes.string, - room: PropTypes.shape({ - name: PropTypes.string.isRequired, - membership: PropTypes.string.isRequired, - messages: ImmutablePropTypes.list.isRequired, - showUsers: PropTypes.bool, - }), - roomActions: PropTypes.shape({ - join: PropTypes.func.isRequired, - sendMessage: PropTypes.func.isRequired, - }).isRequired, -}; - export default RoomChat; diff --git a/src/components/RoomChatHeader.js b/src/components/RoomChatHeader.js index 149cf5d..45ded3a 100644 --- a/src/components/RoomChatHeader.js +++ b/src/components/RoomChatHeader.js @@ -1,6 +1,5 @@ import React, { PropTypes } from "react"; import { withRouter } from "react-router"; -import ImmutablePropTypes from "react-immutable-proptypes"; const make_header = (title, showUsersButton, leaveButton) => (
@@ -46,16 +45,4 @@ const RoomChatHeader = ({ room, roomActions, router }) => { } }; -RoomChatHeader.propTypes = { - room: PropTypes.shape({ - membership: PropTypes.string.isRequired, - name: PropTypes.string.isRequired, - showUsers: PropTypes.bool, - }), - roomActions: PropTypes.shape({ - leave: PropTypes.func.isRequired, - }).isRequired, - router: PropTypes.object.isRequired, -}; - export default withRouter(RoomChatHeader); diff --git a/src/components/RoomList.js b/src/components/RoomList.js deleted file mode 100644 index f8e62e4..0000000 --- a/src/components/RoomList.js +++ /dev/null @@ -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 }) => ( - -); - -RoomList.propTypes = { - rooms: ImmutablePropTypes.record.isRequired, - roomActions: PropTypes.shape({ - getList: PropTypes.func.isRequired, - }).isRequired, -}; - -export default RoomList; diff --git a/src/components/RoomList.tsx b/src/components/RoomList.tsx new file mode 100644 index 0000000..ef92822 --- /dev/null +++ b/src/components/RoomList.tsx @@ -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 = ({ rooms }) => { + const dispatch = useDispatch(); + const refresh = () => { + dispatch( + socketSendMessage({ + variant: "RoomListRequest", + fields: [], + }) + ); + }; + + return ( + + id="room-list" + map={rooms} + refresh={refresh} + /> + ); +}; + +export default RoomList; diff --git a/src/components/RoomUserList.js b/src/components/RoomUserList.js index 884ec48..d355647 100644 --- a/src/components/RoomUserList.js +++ b/src/components/RoomUserList.js @@ -16,8 +16,4 @@ const RoomUserList = ({ users }) => { return
    {children}
; }; -RoomUserList.propTypes = { - users: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired, -}; - export default RoomUserList; diff --git a/src/components/SearchableList.js b/src/components/SearchableList.js deleted file mode 100644 index 63aecd5..0000000 --- a/src/components/SearchableList.js +++ /dev/null @@ -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( -
  • - -
  • - ); - } - - const onClick = (event) => { - event.preventDefault(); - refresh(); - }; - - return ( -
    -
    -
    - -
    -
    -
      {children}
    -
    - ); - } - } - - ComposedSearchableList.propTypes = { - id: PropTypes.string.isRequired, - itemMap: ImmutablePropTypes.record.isRequired, - refresh: PropTypes.func.isRequired, - }; - - return ComposedSearchableList; -}; - -export default SearchableList; diff --git a/src/components/SearchableList.tsx b/src/components/SearchableList.tsx new file mode 100644 index 0000000..7e9a1ab --- /dev/null +++ b/src/components/SearchableList.tsx @@ -0,0 +1,43 @@ +import { ComponentType, FC } from "react"; + +interface Props { + id: string; + map: { [key: string]: Item }; + refresh: () => void; +} + +function SearchableList< + Item, + ItemComponent extends ComponentType> +>({ id, map, refresh }: Props) { + const children = []; + + for (const name in map) { + children.push( +
  • + +
  • + ); + } + + const onClick = (event) => { + event.preventDefault(); + refresh(); + }; + + return ( +
    +
    +
    + +
    +
    +
      {children}
    +
    + ); +} + +// eslint-disable-next-line +let _assertType: FC> = SearchableList; + +export default SearchableList; diff --git a/src/components/SolsticeApp.js b/src/components/SolsticeApp.js deleted file mode 100644 index f4c3c9d..0000000 --- a/src/components/SolsticeApp.js +++ /dev/null @@ -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 = ({ socket, children }) => { - const location = useLocation(); - - if (socket.state !== STATE_OPEN) { - return ( - - ); - } - - return ( -
    -
    -
    {children}
    -
    -
    - ); -}; - -const SolsticeApp = ({ socket, children }) => ( - - - - - - {children} - - -); - -const mapStateToProps = ({ socket }) => ({ socket }); - -export default connect(mapStateToProps)(SolsticeApp); diff --git a/src/components/SolsticeApp.tsx b/src/components/SolsticeApp.tsx new file mode 100644 index 0000000..91b4951 --- /dev/null +++ b/src/components/SolsticeApp.tsx @@ -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 ( +
    + + + + + Coming soon: users pane. + +
    + ); +}; + +const ConnectedApp: FC = ({ children }) => { + const socket = useSelector(selectSocket); + const location = useLocation(); + + if (socket.state !== SocketState.Open) { + return ( + + ); + } + + return children; +}; + +const SolsticeApp = () => ( + + + + + + +
    +
    + +
    +
    +
    +
    +
    +); + +export default SolsticeApp; diff --git a/src/containers/ConnectPage.js b/src/containers/ConnectPage.tsx similarity index 97% rename from src/containers/ConnectPage.js rename to src/containers/ConnectPage.tsx index d7cc2b0..79a123b 100644 --- a/src/containers/ConnectPage.js +++ b/src/containers/ConnectPage.tsx @@ -29,7 +29,7 @@ const ConnectPage: React.FC = () => { return ( diff --git a/src/containers/RoomsPane.js b/src/containers/RoomsPane.js deleted file mode 100644 index 7adc4a8..0000000 --- a/src/containers/RoomsPane.js +++ /dev/null @@ -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 = ( - - ); - } - } - - return ( -
    - -
    {roomChat}
    -
    - ); -}; - -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); diff --git a/src/containers/RoomsPane.tsx b/src/containers/RoomsPane.tsx new file mode 100644 index 0000000..0999e0d --- /dev/null +++ b/src/containers/RoomsPane.tsx @@ -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 = ( + + ); + } + } + */ + + return ( +
    + +
    {roomChat}
    +
    + ); +}; + +export default RoomsPane; diff --git a/src/index.tsx b/src/index.tsx index 0a790c1..f3fea9a 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,4 +1,4 @@ -import React from "react"; +import { StrictMode } from "react"; import ReactDOM from "react-dom"; import { Provider } from "react-redux"; import { BrowserRouter as Router } from "react-router-dom"; @@ -10,12 +10,12 @@ import { store } from "./app/store"; import SolsticeApp from "./components/SolsticeApp"; ReactDOM.render( - + - , + , document.getElementById("root") ); diff --git a/src/modules/room/slice.ts b/src/modules/room/slice.ts new file mode 100644 index 0000000..a7db6e0 --- /dev/null +++ b/src/modules/room/slice.ts @@ -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; + members: Set; + 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 + ) => { + 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) => { + 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; diff --git a/tsconfig.json b/tsconfig.json index a273b0c..9d379a3 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,11 +1,7 @@ { "compilerOptions": { "target": "es5", - "lib": [ - "dom", - "dom.iterable", - "esnext" - ], + "lib": ["dom", "dom.iterable", "esnext"], "allowJs": true, "skipLibCheck": true, "esModuleInterop": true, @@ -20,7 +16,5 @@ "noEmit": true, "jsx": "react-jsx" }, - "include": [ - "src" - ] + "include": ["src"] }