diff --git a/frontend/package.json b/frontend/package.json index 016cc452e20bd021a030107a605f1fa7672388d2..9eb6c36b0fdb81f5ab12c79968ebdee91d050abe 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -20,7 +20,7 @@ "redux-saga": "^1.1.3", "redux-thunk": "^2.3.0", "styled-components": "^5.2.1", - "typescript": "^4.0.5", + "typescript": "4.0.5", "web-vitals": "^0.2.4" }, "scripts": { diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 76bf3f23b6bdc52c38cbef8df745e9230f884ea3..8699b4af615ecb58daad98da5ddb24b19b5b9de4 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -8,10 +8,12 @@ import SyncAllButton from "./player/SyncAllButton"; import "./App.css"; import UrlBox from "./url/UrlBox"; import { Box, Container, Grid } from "@material-ui/core"; +import ConnectionStatusBar from "./connectionstatus/ConnectionStatusBar"; function App() { return ( <Container maxWidth={window.innerWidth >= 2000 ? "xl" : "lg"}> + <ConnectionStatusBar /> <Box my={1}> <Grid container spacing={1}> <Grid item xs={12}> diff --git a/frontend/src/backendconnection/communication.ts b/frontend/src/backendconnection/communication.ts index 2b7762b6e0afa602050d6461cfcc1a65ac190d87..14fe03a01b19add15d4881d168e692a6c2da16ed 100644 --- a/frontend/src/backendconnection/communication.ts +++ b/frontend/src/backendconnection/communication.ts @@ -3,7 +3,10 @@ import { selectSeconds, setPlaySeconds, } from "../player/PlayerSlice"; -import { put, select, takeEvery } from "redux-saga/effects"; + +import { setConnectionStatus } from "../connectionstatus/ConnectionStatusSlice"; + +import { all, put, select, takeEvery } from "redux-saga/effects"; export const requestPlay = { type: "REDUX_WEBSOCKET::SEND", @@ -49,6 +52,13 @@ export type backendMessageFormat = { }; }; +export type connectionStatusFormat = { + type: "REDUX_WEBSOCKET::OPEN" | "REDUX_WEBSOCKET::CLOSED"; + meta: { + timestamp: Date; + }; +}; + // worker Saga: will be fired on USER_FETCH_REQUESTED actions function* fetchUser(action: any) { try { @@ -87,39 +97,35 @@ function* fetchUser(action: any) { } } -/* - Starts fetchUser on each dispatched `USER_FETCH_REQUESTED` action. - Allows concurrent fetches of user. -*/ -function* mySaga() { +function* messageSaga() { yield takeEvery("REDUX_WEBSOCKET::MESSAGE", fetchUser); } -/* - Alternatively you may use takeLatest. - - Does not allow concurrent fetches of user. If "USER_FETCH_REQUESTED" gets - dispatched while a fetch is already pending, that pending fetch is cancelled - and only the latest one will be run. -*/ +function* handleConnectionStatus(action: any) { + let connectionMessage = action as connectionStatusFormat; -export default mySaga; - -//export + switch (connectionMessage.type) { + case "REDUX_WEBSOCKET::OPEN": { + yield put(setConnectionStatus(true)); + break; + } + case "REDUX_WEBSOCKET::CLOSED": { + yield put(setConnectionStatus(false)); + break; + } + } + yield; +} -/* -export function (){ - return send({command: "pause", target: "server"}); +function* connectionStatusSaga() { + // "REDUX_WEBSOCKET::BROKEN" is not neccessary, because it comes after CLOSED + // Maybe add ERROR + yield takeEvery( + ["REDUX_WEBSOCKET::OPEN", "REDUX_WEBSOCKET::CLOSED"], + handleConnectionStatus + ); } -{ - type: 'REDUX_WEBSOCKET::MESSAGE', - meta: { - timestamp: Date, - }, - payload: { - message: string, - origin: string, - }, +export default function* rootSaga() { + yield all([connectionStatusSaga(), messageSaga()]); } -*/ diff --git a/frontend/src/connectionstatus/ConnectionStatusBar.tsx b/frontend/src/connectionstatus/ConnectionStatusBar.tsx new file mode 100644 index 0000000000000000000000000000000000000000..5fc0d493640212fa1dbdaffe2327808fa8394336 --- /dev/null +++ b/frontend/src/connectionstatus/ConnectionStatusBar.tsx @@ -0,0 +1,51 @@ +import { connect } from "react-redux"; +import { RootState } from "../store/store"; +import { selectConnected } from "./ConnectionStatusSlice"; + +import React from "react"; +import { createStyles, makeStyles, Theme } from "@material-ui/core/styles"; +import AppBar from "@material-ui/core/AppBar"; +import Toolbar from "@material-ui/core/Toolbar"; +import Typography from "@material-ui/core/Typography"; + +const useStyles = makeStyles((theme: Theme) => + createStyles({ + root: { + flexGrow: 1, + }, + title: { + flexGrow: 1, + }, + }) +); + +interface StateProps { + connected: boolean; +} + +export type Props = StateProps; + +export function ConnectionStatusBar( + props: Props +) { + const classes = useStyles(); + return ( + <div className={classes.root}> + <AppBar position="static" color="default"> + <Toolbar> + <Typography variant="h6" className={classes.title}> + {props.connected + ? "Connected to server" + : "Lost connection to server"} + </Typography> + </Toolbar> + </AppBar> + </div> + ); +} + +const mapStateToProps = (state: RootState): StateProps => ({ + connected: selectConnected(state), +}); + +export default connect(mapStateToProps)(ConnectionStatusBar); diff --git a/frontend/src/connectionstatus/ConnectionStatusSlice.ts b/frontend/src/connectionstatus/ConnectionStatusSlice.ts new file mode 100644 index 0000000000000000000000000000000000000000..1f96414802a85cf79b0794794858f896ed914677 --- /dev/null +++ b/frontend/src/connectionstatus/ConnectionStatusSlice.ts @@ -0,0 +1,32 @@ +import { createSlice, PayloadAction } from "@reduxjs/toolkit"; +import { RootState } from "../store/store"; + +export interface ConnectionStatusState { + connected: boolean; +} + +export const initialState: ConnectionStatusState = { + connected: false, +}; + +export type ConnectionStatusAction = PayloadAction<boolean>; + +export const ConnectionStatusStateSlice = createSlice({ + name: "connectionStatus", + initialState, + reducers: { + setConnectionStatus: ( + state: ConnectionStatusState, + action: ConnectionStatusAction + ): void => { + state.connected = action.payload; + }, + }, +}); + +export const { setConnectionStatus } = ConnectionStatusStateSlice.actions; + +export const selectConnected = (state: RootState) => + state.connectionStatus.connected; + +export default ConnectionStatusStateSlice.reducer; diff --git a/frontend/src/store/store.ts b/frontend/src/store/store.ts index 69a3684c8ee02f88ec3dfda80d66753ab0bc0ac1..38f9b540b06ed8c795228c3649b9341408460583 100644 --- a/frontend/src/store/store.ts +++ b/frontend/src/store/store.ts @@ -2,9 +2,10 @@ import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit"; import reduxWebsocket from "@giantmachines/redux-websocket"; import PlayerStateReducer from "../player/PlayerSlice"; +import ConnectionStatusStateReducer from "../connectionstatus/ConnectionStatusSlice"; import UrlReducer from "../url/UrlSlice"; -import mySaga from "../backendconnection/communication"; +import rootSaga from "../backendconnection/communication"; import createSagaMiddleware from "redux-saga"; @@ -12,6 +13,7 @@ import { connect } from "@giantmachines/redux-websocket"; // Create the websocket instance. const reduxWebsocketMiddleware = reduxWebsocket({ + reconnectInterval: 1000, reconnectOnClose: true, dateSerializer: (date: Date) => "haha", }); @@ -29,6 +31,7 @@ export const store = configureStore({ reducer: { url: UrlReducer, player: PlayerStateReducer, + connectionStatus: ConnectionStatusStateReducer, }, middleware: [ ...getDefaultMiddleware({ @@ -37,6 +40,12 @@ export const store = configureStore({ // Ignore these action types ignoredActions: [ "REDUX_WEBSOCKET::CONNECT", + "REDUX_WEBSOCKET::CLOSED", + "REDUX_WEBSOCKET::ERROR", + "REDUX_WEBSOCKET::RECONNECT_ATTEMPT", + "REDUX_WEBSOCKET::RECONNECTED", + "REDUX_WEBSOCKET::BEGIN_RECONNECT", + "REDUX_WEBSOCKET::BROKEN", "REDUX_WEBSOCKET::OPEN", "REDUX_WEBSOCKET::MESSAGE", "REDUX_WEBSOCKET::SEND", @@ -49,6 +58,6 @@ export const store = configureStore({ }); store.dispatch(connect("wss://mpvsync.de:8432/")); -sagaMiddleware.run(mySaga); +sagaMiddleware.run(rootSaga); export type RootState = ReturnType<typeof store.getState>;