Browse Source

Fix compile failures after move to tsx.

main
Titouan Rigoudy 4 years ago
parent
commit
a41529d52e
13 changed files with 163 additions and 121 deletions
  1. +21
    -9
      src/app/store.ts
  2. +1
    -1
      src/components/ConnectForm.tsx
  3. +5
    -5
      src/components/Header.tsx
  4. +4
    -8
      src/components/RoomList.tsx
  5. +32
    -29
      src/components/SearchableList.tsx
  6. +2
    -1
      src/components/SolsticeApp.tsx
  7. +4
    -4
      src/modules/login/message.ts
  8. +1
    -4
      src/modules/login/slice.ts
  9. +22
    -12
      src/modules/room/slice.ts
  10. +0
    -5
      src/modules/websocket/actions.ts
  11. +4
    -2
      src/modules/websocket/message.ts
  12. +66
    -40
      src/modules/websocket/middleware.ts
  13. +1
    -1
      src/modules/websocket/slice.ts

+ 21
- 9
src/app/store.ts View File

@ -1,4 +1,9 @@
import { configureStore, ThunkAction, Action } from "@reduxjs/toolkit";
import {
combineReducers,
configureStore,
ThunkAction,
Action,
} from "@reduxjs/toolkit";
import counterReducer from "../features/counter/counterSlice";
// TODO: Rework these to use a slice, otherwise redux complains about
@ -13,14 +18,22 @@ import roomReducer from "../modules/room/slice";
import makeSocketMiddleware from "../modules/websocket/middleware";
import socketReducer from "../modules/websocket/slice";
const rootReducer = combineReducers({
counter: counterReducer,
login: loginReducer,
rooms: roomReducer,
socket: socketReducer,
//users,
});
// Define this before defining `store`, to break a circular type reference:
//
// store -> middleware -> RootState
//
export type RootState = ReturnType<typeof rootReducer>;
export const store = configureStore({
reducer: {
counter: counterReducer,
login: loginReducer,
rooms: roomReducer,
socket: socketReducer,
//users,
},
reducer: rootReducer,
middleware: (getDefaultMiddleware) =>
getDefaultMiddleware().concat(
makeSocketMiddleware(loginSocketMessageHandlers)
@ -28,7 +41,6 @@ export const store = configureStore({
});
export type AppDispatch = typeof store.dispatch;
export type RootState = ReturnType<typeof store.getState>;
export type AppThunk<ReturnType = void> = ThunkAction<
ReturnType,
RootState,


+ 1
- 1
src/components/ConnectForm.tsx View File

@ -17,7 +17,7 @@ interface Props {
const ConnectForm: FC<Props> = ({ socket }) => {
const dispatch = useDispatch();
const onSubmit = ({ url }) => {
const onSubmit = ({ url }: { url: string }) => {
dispatch(socketOpen(url));
};


+ 5
- 5
src/components/Header.tsx View File

@ -1,15 +1,15 @@
import { FC } from "react";
import { Link } from "react-router-dom";
import { NavLink } from "react-router-dom";
const Header: FC = () => (
<header>
<h1>Solstice web UI</h1>
<Link to="/rooms" activeClassName="active">
<NavLink to="/rooms" activeClassName="active">
Rooms
</Link>
<Link to="/users" activeClassName="active">
</NavLink>
<NavLink to="/users" activeClassName="active">
Users
</Link>
</NavLink>
</header>
);


+ 4
- 8
src/components/RoomList.tsx View File

@ -3,9 +3,11 @@ import { useDispatch } from "react-redux";
import RoomComponent from "./Room";
import SearchableList from "./SearchableList";
import { Room, RoomSliceState } from "../modules/room/slice";
import { RoomSliceState } from "../modules/room/slice";
import { socketSendMessage } from "../modules/websocket/slice";
const SearchableRoomList = SearchableList(RoomComponent);
const RoomList: FC<RoomSliceState> = ({ rooms }) => {
const dispatch = useDispatch();
const refresh = () => {
@ -17,13 +19,7 @@ const RoomList: FC<RoomSliceState> = ({ rooms }) => {
);
};
return (
<SearchableList<Room, RoomComponent>
id="room-list"
map={rooms}
refresh={refresh}
/>
);
return <SearchableRoomList id="room-list" map={rooms} refresh={refresh} />;
};
export default RoomList;

+ 32
- 29
src/components/SearchableList.tsx View File

@ -1,43 +1,46 @@
import { ComponentType, FC } from "react";
import { ComponentType, FC, ReactEventHandler } from "react";
interface Props<Item> {
interface ItemProps<Item> {
name: string;
data: Item;
}
interface ListProps<Item> {
id: string;
map: { [key: string]: Item };
refresh: () => void;
}
function SearchableList<
Item,
ItemComponent extends ComponentType<Props<Item>>
>({ id, map, refresh }: Props<Item>) {
const children = [];
function SearchableList<Item>(
ItemComponent: ComponentType<ItemProps<Item>>
): FC<ListProps<Item>> {
return ({ id, map, refresh }: ListProps<Item>) => {
const children = [];
for (const name in map) {
children.push(
<li key={name}>
<ItemComponent name={name} data={map[name]} />
</li>
);
}
for (const name in map) {
children.push(
<li key={name}>
<ItemComponent name={name} data={map[name]} />
</li>
);
}
const onClick = (event) => {
event.preventDefault();
refresh();
};
const onClick: ReactEventHandler = (event) => {
event.preventDefault();
refresh();
};
return (
<div id={id}>
<div id={`${id}-header`}>
<div>
<button onClick={onClick}>Refresh</button>
return (
<div id={id}>
<div id={`${id}-header`}>
<div>
<button onClick={onClick}>Refresh</button>
</div>
</div>
<ul>{children}</ul>
</div>
<ul>{children}</ul>
</div>
);
);
};
}
// eslint-disable-next-line
let _assertType: FC<Props<Item>> = SearchableList;
export default SearchableList;

+ 2
- 1
src/components/SolsticeApp.tsx View File

@ -44,7 +44,8 @@ const ConnectedApp: FC = ({ children }) => {
);
}
return children;
// TODO: eliminate the need for this extra <div> wrapper.
return <div>{children}</div>;
};
const SolsticeApp = () => (


+ 4
- 4
src/modules/login/message.ts View File

@ -1,5 +1,5 @@
import { loginSetState } from "../login/slice";
import { SocketMessage } from "../websocket/message";
import { loginSetState, LoginStatus } from "../login/slice";
import { SocketMessage, SocketMessageHandler } from "../websocket/message";
export function loginStatusRequest(): SocketMessage {
return {
@ -21,7 +21,7 @@ const loginStatusResponseHandler: SocketMessageHandler = {
});
}
case "Success": {
const [username, motd] = data.fields;
const [username, motd] = fields;
return loginSetState({
status: LoginStatus.Success,
username,
@ -30,7 +30,7 @@ const loginStatusResponseHandler: SocketMessageHandler = {
}
case "Failure": {
const [username, reason] = data.fields;
const [username, reason] = fields;
return loginSetState({
status: LoginStatus.Failure,
username,


+ 1
- 4
src/modules/login/slice.ts View File

@ -19,9 +19,6 @@ export interface LoginSliceState {
const initialState: LoginSliceState = {
status: LoginStatus.Unknown,
url: undefined,
motd: undefined,
reason: undefined,
};
export const loginSlice = createSlice({
@ -29,7 +26,7 @@ export const loginSlice = createSlice({
initialState,
reducers: {
loginSetState: (
state: RootState,
state: LoginSliceState,
action: PayloadAction<LoginSliceState>
) => {
state = action.payload;


+ 22
- 12
src/modules/room/slice.ts View File

@ -9,6 +9,11 @@ export enum RoomMembership {
Left,
}
export interface RoomMessage {
user_name: string;
message: string;
}
export interface Room {
name: string;
membership: RoomMembership;
@ -18,18 +23,24 @@ export interface Room {
owner: string;
operators: Set<string>;
members: Set<string>;
messages: string[];
messages: RoomMessage[];
tickers: string[];
// showUsers: boolean;
}
export interface RoomSliceState {
export interface RoomMap {
[name: string]: Room;
}
const initialState: RoomSliceState = {};
export interface RoomSliceState {
rooms: RoomMap;
}
export interface RoomMessage {
const initialState: RoomSliceState = {
rooms: {},
};
export interface RoomMessagePayload {
room_name: string;
user_name: string;
message: string;
@ -44,7 +55,7 @@ export const roomSlice = createSlice({
action: PayloadAction<[string, RoomMembership]>
) => {
const [name, membership] = action.payload;
const room = state[name];
const room = state.rooms[name];
if (room === undefined) {
console.log(`Cannot set membership of room ${name}`);
return;
@ -54,10 +65,10 @@ export const roomSlice = createSlice({
},
roomMessage: (
state: RoomSliceState,
action: PayloadAction<RoomMessage>
action: PayloadAction<RoomMessagePayload>
) => {
const { room_name, user_name, message } = action.payload;
const room = state[room_name];
const room = state.rooms[room_name];
if (room === undefined) {
console.log(
`Unknown room ${room_name} received message from ` +
@ -71,7 +82,7 @@ export const roomSlice = createSlice({
roomSetAll: (state: RoomSliceState, action: PayloadAction<Room[]>) => {
state.rooms = {};
for (const room of action.payload) {
state[room.name] = room;
state.rooms[room.name] = room;
}
},
},
@ -79,9 +90,8 @@ export const roomSlice = createSlice({
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 function selectAllRooms(state: RootState): RoomMap {
return state.rooms.rooms;
}
export default roomSlice.reducer;

+ 0
- 5
src/modules/websocket/actions.ts View File

@ -1,5 +0,0 @@
export const wsConnect = (host) => ({ type: "WS_CONNECT", host });
export const wsConnecting = (host) => ({ type: "WS_CONNECTING", host });
export const wsConnected = (host) => ({ type: "WS_CONNECTED", host });
export const wsDisconnect = () => ({ type: "WS_DISCONNECT" });
export const wsDisconnected = () => ({ type: "WS_DISCONNECTED" });

+ 4
- 2
src/modules/websocket/message.ts View File

@ -5,7 +5,7 @@ import { PayloadAction } from "@reduxjs/toolkit";
// The type of a message exchanged over a websocket.
export interface SocketMessage {
variant: string;
fields?: List<any>;
fields: any[];
}
// Serializes the given message for sending over the wire.
@ -25,7 +25,9 @@ export function parseMessage(serialized: string): SocketMessage {
}
// Handles the `data` field of a `SocketMessage` and returns an optional action.
export type SocketMessageDataHandler = (any) => PayloadAction<any> | undefined;
export type SocketMessageDataHandler = (
data: any
) => PayloadAction<any> | undefined;
// A message handler creates an action out of a socket message.
//


+ 66
- 40
src/modules/websocket/middleware.ts View File

@ -17,24 +17,24 @@ import {
socketClosed,
socketSendMessage,
} from "./slice";
import { AppDispatch, RootState } from "../app/store";
import { AppDispatch, RootState } from "../../app/store";
// The WebSocket singleton.
// TODO: use undefined instead, it is more idiomatic.
let socket: WebSocket | null = null;
let socket: WebSocket | undefined;
const onOpen = (dispatch: AppDispatch) => (event) => {
console.log("Websocket open", event.target.url);
dispatch(socketOpened(event.target.url));
const makeOnOpen = (dispatch: AppDispatch) => (event: Event) => {
const target = event.target as WebSocket;
console.log("WebSocket open", target.url);
dispatch(socketOpened(target.url));
};
const onClose = (dispatch: AppDispatch) => () => {
const makeOnClose = (dispatch: AppDispatch) => () => {
dispatch(socketClosed());
};
function prepareHandlers(
handlers: SocketMessageHandler[]
): Map<string, SocketMessageDataHandler[]> {
type HandlerMap = Map<string, SocketMessageDataHandler[]>;
function prepareHandlers(handlers: SocketMessageHandler[]): HandlerMap {
const map = new Map();
for (const handler of handlers) {
@ -52,16 +52,15 @@ function prepareHandlers(
return map;
}
const onMessage =
(dispatch: AppDispatch, handlers: Map<string, SocketMessageDataHandler[]>) =>
(event: MessageEvent) => {
console.log("Websocket received message", event.data);
const makeOnMessage =
(dispatch: AppDispatch, handlers: HandlerMap) => (event: MessageEvent) => {
console.log("WebSocket received message", event.data);
let message;
try {
message = parseMessage(event.data);
} catch (err) {
console.log("Websocket received invalid message:", err);
console.log("WebSocket received invalid message:", err);
return;
}
@ -70,7 +69,7 @@ const onMessage =
return;
}
for (handler of list) {
for (const handler of list) {
const action = handler(message);
if (action !== undefined) {
dispatch(action);
@ -78,38 +77,65 @@ const onMessage =
}
};
type SocketFactory = (url: string) => WebSocket;
function makeSocketFactory(
dispatch: AppDispatch,
handlers: HandlerMap
): SocketFactory {
const onOpen = makeOnOpen(dispatch);
const onClose = makeOnClose(dispatch);
const onMessage = makeOnMessage(dispatch, handlers);
return (url) => {
socket = new WebSocket(url);
// Bind websocket handlers.
socket.onopen = onOpen;
socket.onclose = onClose;
socket.onmessage = onMessage;
return socket;
};
}
// See: https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
function makeMiddleware(
handlers: SocketMessageHandler[]
): Middleware<{}, RootState> {
const handlerMap = prepareHandlers(handlers);
return (storeApi) => (next) => (action) => {
if (socketOpen.match(action)) {
if (socket !== null) {
socket.close();
return (storeApi) => {
const socketFactory = makeSocketFactory(storeApi.dispatch, handlerMap);
return (next) => (action) => {
if (socketOpen.match(action)) {
if (socket === undefined) {
const url = action.payload;
console.log(`Opening WebSocket to ${url}`);
socket = socketFactory(url);
} else {
console.log("Ignoring socketOpen action, socket is already open.");
}
} else if (socketClose.match(action)) {
if (socket !== undefined) {
socket.close();
socket = undefined;
console.log("WebSocket closed.");
} else {
console.log("Ignoring socketClose action, socket is already closed.");
}
} else if (socketSendMessage.match(action)) {
if (socket !== undefined) {
console.log("WebSocket sending message", action.payload);
socket.send(JSON.stringify(action.payload));
} else {
console.log("Ignoring socketSendMessage action, socket is closed.");
}
}
// Connect to the remote host.
socket = new WebSocket(action.payload);
// Bind websocket handlers.
socket.onmessage = onMessage(storeApi.dispatch, handlerMap);
socket.onclose = onClose(storeApi.dispatch);
socket.onopen = onOpen(storeApi.dispatch);
} else if (socketClose.match(action)) {
if (socket !== null) {
socket.close();
}
socket = null;
console.log("Websocket closed.");
} else if (socketSendMessage.match(action)) {
console.log("Websocket sending message", action.payload);
socket.send(JSON.stringify(action.payload));
}
return next(action);
return next(action);
};
};
}


+ 1
- 1
src/modules/websocket/slice.ts View File

@ -27,7 +27,7 @@ export const socketSlice = createSlice({
state.state = SocketState.Opening;
state.url = action.payload;
},
socketOpened: (state, action: PayoadAction<string>) => {
socketOpened: (state, action: PayloadAction<string>) => {
state.state = SocketState.Open;
state.url = action.payload;
},


Loading…
Cancel
Save