Browse Source

Implement rough first parts of /rooms.

main
Titouan Rigoudy 4 years ago
parent
commit
3473b396af
19 changed files with 297 additions and 265 deletions
  1. +2
    -1
      src/app/store.ts
  2. +0
    -18
      src/components/Header.js
  3. +16
    -0
      src/components/Header.tsx
  4. +1
    -7
      src/components/Room.js
  5. +0
    -15
      src/components/RoomChat.js
  6. +0
    -13
      src/components/RoomChatHeader.js
  7. +0
    -24
      src/components/RoomList.js
  8. +29
    -0
      src/components/RoomList.tsx
  9. +0
    -4
      src/components/RoomUserList.js
  10. +0
    -57
      src/components/SearchableList.js
  11. +43
    -0
      src/components/SearchableList.tsx
  12. +0
    -47
      src/components/SolsticeApp.js
  13. +67
    -0
      src/components/SolsticeApp.tsx
  14. +1
    -1
      src/containers/ConnectPage.tsx
  15. +0
    -67
      src/containers/RoomsPane.js
  16. +46
    -0
      src/containers/RoomsPane.tsx
  17. +3
    -3
      src/index.tsx
  18. +87
    -0
      src/modules/room/slice.ts
  19. +2
    -8
      tsconfig.json

+ 2
- 1
src/app/store.ts View File

@ -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,
},


+ 0
- 18
src/components/Header.js View File

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

+ 16
- 0
src/components/Header.tsx View File

@ -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
- 7
src/components/Room.js View File

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

+ 0
- 15
src/components/RoomChat.js View File

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

+ 0
- 13
src/components/RoomChatHeader.js View File

@ -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) => (
<div id="room-chat-header">
@ -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);

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

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

+ 29
- 0
src/components/RoomList.tsx View File

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

+ 0
- 4
src/components/RoomUserList.js View File

@ -16,8 +16,4 @@ const RoomUserList = ({ users }) => {
return <ul id="room-user-list">{children}</ul>;
};
RoomUserList.propTypes = {
users: PropTypes.arrayOf(PropTypes.string.isRequired).isRequired,
};
export default RoomUserList;

+ 0
- 57
src/components/SearchableList.js View File

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

+ 43
- 0
src/components/SearchableList.tsx View File

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

+ 0
- 47
src/components/SolsticeApp.js View File

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

+ 67
- 0
src/components/SolsticeApp.tsx View File

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

src/containers/ConnectPage.js → src/containers/ConnectPage.tsx View File

@ -29,7 +29,7 @@ const ConnectPage: React.FC = () => {
return (
<Redirect
to={{
pathname: "/app/rooms",
pathname: "/rooms",
state: { from: location },
}}
/>

+ 0
- 67
src/containers/RoomsPane.js View File

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

+ 46
- 0
src/containers/RoomsPane.tsx View File

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

+ 3
- 3
src/index.tsx View File

@ -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(
<React.StrictMode>
<StrictMode>
<Provider store={store}>
<Router>
<SolsticeApp />
</Router>
</Provider>
</React.StrictMode>,
</StrictMode>,
document.getElementById("root")
);

+ 87
- 0
src/modules/room/slice.ts View File

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

+ 2
- 8
tsconfig.json View File

@ -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"]
}

Loading…
Cancel
Save