From 27ae12a606e534833cbe0b8e21eb83218f69dc9e Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 22 May 2024 15:15:09 -0500 Subject: [PATCH 1/4] feat: update disconnected message Now features an error message if one exists, along with a reconnect button for those situations that require one. --- .../DisconnectedMessage.module.scss | 3 +++ .../DisconnectedMessage.tsx | 18 ++++++++++++------ src/lib/store/ui/ui.slice.ts | 10 ++++++++++ src/lib/store/ui/uiSelectors.ts | 4 ++++ 4 files changed, 29 insertions(+), 6 deletions(-) create mode 100644 src/lib/shared/disconnectedMessage/DisconnectedMessage.module.scss diff --git a/src/lib/shared/disconnectedMessage/DisconnectedMessage.module.scss b/src/lib/shared/disconnectedMessage/DisconnectedMessage.module.scss new file mode 100644 index 0000000..e8ef54c --- /dev/null +++ b/src/lib/shared/disconnectedMessage/DisconnectedMessage.module.scss @@ -0,0 +1,3 @@ +.mwfit { + max-width: fit-content; +} \ No newline at end of file diff --git a/src/lib/shared/disconnectedMessage/DisconnectedMessage.tsx b/src/lib/shared/disconnectedMessage/DisconnectedMessage.tsx index e1afd79..264b0a6 100644 --- a/src/lib/shared/disconnectedMessage/DisconnectedMessage.tsx +++ b/src/lib/shared/disconnectedMessage/DisconnectedMessage.tsx @@ -1,12 +1,18 @@ +import { useError, useShowReconnect, useWebsocketContext } from "src/lib"; +import classes from './DisconnectedMessage.module.scss'; + const DisconnectedMessage = () => { - - return<> + const { reconnect } = useWebsocketContext(); + const errorMessage = useError(); + const showReconnect = useShowReconnect(); -
-

Disconnected

-

Reconnecting...

-
+ return<> +
+

Disconnected

+ {errorMessage &&
{errorMessage}
} + {showReconnect && } +
} diff --git a/src/lib/store/ui/ui.slice.ts b/src/lib/store/ui/ui.slice.ts index 1ceb111..36eb040 100644 --- a/src/lib/store/ui/ui.slice.ts +++ b/src/lib/store/ui/ui.slice.ts @@ -1,6 +1,8 @@ import { PayloadAction, createSlice } from '@reduxjs/toolkit'; const initialState: UiConfigState = { + showReconnect: false, + error: '', modalVisibility: { showShutdownModal: false, showIncomingCallModal: false, @@ -38,10 +40,18 @@ const uiSlice = createSlice({ state.popoverVisibility[action.payload.popoverGroup][action.payload.popoverId] = action.payload.value; }, + setErrorMessage(state, action: PayloadAction) { + state.error = action.payload; + }, + setShowReconnect(state, action: PayloadAction) { + state.showReconnect = action.payload; + } } }) export interface UiConfigState { + showReconnect: boolean; + error: string; modalVisibility: Record; popoverVisibility: Record>; } diff --git a/src/lib/store/ui/uiSelectors.ts b/src/lib/store/ui/uiSelectors.ts index 643c8a1..eeeb69e 100644 --- a/src/lib/store/ui/uiSelectors.ts +++ b/src/lib/store/ui/uiSelectors.ts @@ -17,3 +17,7 @@ export const useGetCurrentPopoverIdForGroup = (popoverGroup: string) => useAppSe }); export const useShowPopoverById = (popoverGroup: string, popoverId: string) => useAppSelector((state) => state.ui.popoverVisibility[popoverGroup]?.[popoverId]); + +export const useError = () => useAppSelector((state) => state.ui.error); + +export const useShowReconnect = () => useAppSelector((state) => state.ui.showReconnect); From c0650bb5e7702f074e81e8bbc92f3406f6cdd771 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 22 May 2024 15:16:05 -0500 Subject: [PATCH 2/4] feat: add existing MC logic Handles user code changes and system disconnections when using MC Edge server --- .../store/runtimeConfig/runtimeSelectors.ts | 8 +- src/lib/utils/WebsocketContext.ts | 2 + src/lib/utils/WebsocketProvider.tsx | 147 +++++++++++++----- 3 files changed, 116 insertions(+), 41 deletions(-) diff --git a/src/lib/store/runtimeConfig/runtimeSelectors.ts b/src/lib/store/runtimeConfig/runtimeSelectors.ts index 381afb2..ed11130 100644 --- a/src/lib/store/runtimeConfig/runtimeSelectors.ts +++ b/src/lib/store/runtimeConfig/runtimeSelectors.ts @@ -4,4 +4,10 @@ export const useWsIsConnected = () => useAppSelector((state) => state.runtimeCon export const useRoomKey = () => useAppSelector((state) => state.runtimeConfig.currentRoomKey); -export const useClientId = () => useAppSelector((state) => state.runtimeConfig.roomData.clientId); \ No newline at end of file +export const useClientId = () => useAppSelector((state) => state.runtimeConfig.roomData.clientId); + +export const useSystemUuid = () => useAppSelector((state) => state.runtimeConfig.roomData.systemUuid); + +export const useUserCode = () => useAppSelector((state) => state.runtimeConfig.roomData.userCode); + +export const useServerIsRunningOnProcessorHardware = () => useAppSelector((state) => state.runtimeConfig.serverIsRunningOnProcessorHardware); \ No newline at end of file diff --git a/src/lib/utils/WebsocketContext.ts b/src/lib/utils/WebsocketContext.ts index b9a9401..347e1a3 100644 --- a/src/lib/utils/WebsocketContext.ts +++ b/src/lib/utils/WebsocketContext.ts @@ -6,6 +6,7 @@ export interface WebsocketContextType { sendSimpleMessage: (type: string, payload: boolean | number | string ) => void; addEventHandler: (eventType: string, key: string, callback: (data: Message) => void) => void; removeEventHandler: (eventType: string, key: string) => void; + reconnect: () => void; } const WebsocketContext = createContext({ @@ -13,6 +14,7 @@ const WebsocketContext = createContext({ sendSimpleMessage: () => null, addEventHandler: () => null, removeEventHandler: () => null, + reconnect: () => null, }); export default WebsocketContext; \ No newline at end of file diff --git a/src/lib/utils/WebsocketProvider.tsx b/src/lib/utils/WebsocketProvider.tsx index 9028825..85420b8 100644 --- a/src/lib/utils/WebsocketProvider.tsx +++ b/src/lib/utils/WebsocketProvider.tsx @@ -1,7 +1,7 @@ import { ReactNode, useCallback, useEffect, useRef, useState } from "react"; import { httpClient, useInitialize } from "../services"; import DisconnectedMessage from "../shared/disconnectedMessage/DisconnectedMessage"; -import { store, useAppConfig } from "../store"; +import { store, uiActions, useAppConfig } from "../store"; import { devicesActions } from "../store/devices/devices.slice"; import { roomsActions } from "../store/rooms/rooms.slice"; import { @@ -11,12 +11,16 @@ import { import { useClientId, useRoomKey, + useServerIsRunningOnProcessorHardware, + useSystemUuid, + useUserCode, useWsIsConnected, } from "../store/runtimeConfig/runtimeSelectors"; -import { Message } from "../types"; +import { Message, RoomData } from "../types"; import sessionStorageKeys from "../types/classes/session-storage-keys"; import WebsocketContext from "./WebsocketContext"; import { loadValue, saveValue } from "./joinParamsService"; +import { AxiosError } from "axios"; /** * The context component that contains the websocket connection and provides the sendMessage function @@ -31,6 +35,10 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { const clientId = useClientId(); const initialize = useInitialize(); const appConfig = useAppConfig(); + const systemUuid = useSystemUuid(); + const userCode = useUserCode(); + const serverisRunningOnProcessorHardware = useServerIsRunningOnProcessorHardware(); + const clientRef = useRef(null); const [waitingToReconnect, setWaitingToReconnect] = useState(); @@ -54,25 +62,44 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { * @param apiPath base path to the api without the token */ const getRoomData = useCallback( - async (apiPath: string) => { - await httpClient - .get(`${apiPath}/ui/joinroom?token=${token}`) - .then((res) => { - if (res.status === 200 && res.data) { - store.dispatch(runtimeConfigActions.setRoomData(res.data)); - } - }) - .catch((err) => { - console.log(err); + async (apiPath: string):Promise => { + try { + const res = await httpClient.get(`${apiPath}/ui/joinroom?token=${token}`); + + if (res.status === 200 && res.data) { + store.dispatch(runtimeConfigActions.setRoomData(res.data)); + return true; + } - if (err.repsonse && err.response.status === 498) { - console.error("Invalid token. Unable to join room"); - } - }); + return false + } + catch (err) { + console.log(err); + + if (err instanceof AxiosError && err.response && err.response.status === 498) { + console.error("Invalid token. Unable to join room"); + store.dispatch(uiActions.setErrorMessage(`Token ${token} is invalid. Unable to join room`)); + return false; + } + + console.error("Error getting room data", err); + + if (err instanceof Error) { + store.dispatch(uiActions.setErrorMessage(err.message)); + } else { + store.dispatch(uiActions.setErrorMessage("Error getting room data")); + } + return false; + } }, [token] ); + const reconnect = useCallback(() => { + const newUrl = `${appConfig.gatewayAppPath}?uuid=${systemUuid}&roomKey=${roomKey}`; + window.location.href = userCode ? `${newUrl}&Code=${userCode}` : newUrl; + }, [appConfig.gatewayAppPath, roomKey, systemUuid, userCode]); + /** * Sends a message to the server */ @@ -117,19 +144,14 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { console.log("event handler removed", eventType, key); } - }, []); + }, []); //* EFFECTS *********************************************************/ useEffect(() => { // Get the join token from the url params or from session storage and sets it as a local state variable const qp = new URLSearchParams(window.location.search); - let joinToken = qp.get("token"); - - // if(!token && !appConfig.enableDev) { - // console.error('No join token found. Unable to continue'); - // return; - // } + let joinToken = qp.get("token"); if (joinToken) { console.log("saving token: ", joinToken); @@ -145,14 +167,19 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { // eslint-disable-next-line react-hooks/exhaustive-deps }, []); + + /** * Connect to the websocket and get the room data when the apiPath changes */ useEffect(() => { async function joinWebsocket() { + console.log('effect is running'); if (!appConfig.apiPath || waitingToReconnect || !token) return; - await getRoomData(appConfig.apiPath); + const tokenResult = await getRoomData(appConfig.apiPath); + + if(!tokenResult) return; if (!clientRef.current) { const wsPath = appConfig.apiPath.replace("http", "ws"); @@ -162,8 +189,8 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { clientRef.current = newWs; - newWs.onopen = () => { - console.log("connected"); + newWs.onopen = (ev: Event) => { + console.log("connected", ev.type, ev.target); store.dispatch(runtimeConfigActions.setWebsocketIsConnected(true)); }; @@ -171,8 +198,40 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { console.log(err); }; - newWs.onclose = () => { - console.log("disconnected"); + newWs.onclose = (closeEvent: CloseEvent): void => { + console.log("disconnected: ", closeEvent.reason, closeEvent.code); + + if (closeEvent.code === 4000) { + console.log("user code changed"); + store.dispatch(runtimeConfigActions.setUserCode({ userCode: '', qrUrl: ''})); + store.dispatch(uiActions.setErrorMessage("User code changed. Click reconnect to enter the new code")); + store.dispatch(uiActions.setShowReconnect(true)); + store.dispatch(runtimeConfigActions.setWebsocketIsConnected(false)); + store.dispatch(devicesActions.clearDevices()); + store.dispatch(roomsActions.clearRooms()); + return; + } + + if (closeEvent.code === 4001 && !serverisRunningOnProcessorHardware) { + console.log("processor disconnected"); + store.dispatch(uiActions.setErrorMessage("Processor has disconnected. Click Reconnect")); + store.dispatch(uiActions.setShowReconnect(true)); + store.dispatch(runtimeConfigActions.setWebsocketIsConnected(false)); + store.dispatch(devicesActions.clearDevices()); + store.dispatch(roomsActions.clearRooms()); + return; + } + + if (closeEvent.code === 4002) { + console.log("room combination changed"); + store.dispatch(uiActions.setErrorMessage("Room combination changed. Click Reconnect to re-join the room")); + store.dispatch(uiActions.setShowReconnect(true)); + store.dispatch(runtimeConfigActions.setWebsocketIsConnected(false)); + store.dispatch(devicesActions.clearDevices()); + store.dispatch(roomsActions.clearRooms()); + return; + } + if (clientRef.current) { console.log("WebSocket closed by server."); } else { @@ -183,10 +242,10 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { if (waitingToReconnect) { return; } - + store.dispatch(runtimeConfigActions.setWebsocketIsConnected(false)); store.dispatch(devicesActions.clearDevices()); - store.dispatch(roomsActions.clearRooms()); + store.dispatch(roomsActions.clearRooms()); setWaitingToReconnect(true); @@ -198,8 +257,13 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { const message: Message = JSON.parse(e.data); console.log(message); + if (message.type === 'close') // MC API sent a close message + { + newWs.close(4001, message.content as string); + return; + } if (message.type.startsWith("/system/")) { - switch (message.type) { + switch (message.type) { case "/system/roomKey": store.dispatch( runtimeConfigActions.setCurrentRoomKey( @@ -251,18 +315,20 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { } }; } - // Cleanup first websocket in dev mode due to double render cycle - return () => { - if (clientRef.current) { - clientRef.current.close(); - } - - clientRef.current = null; - }; } joinWebsocket(); - }, [appConfig.apiPath, getRoomData, token, waitingToReconnect]); + + // Cleanup first websocket in dev mode due to double render cycle + return () => { + if (clientRef.current) { + clientRef.current.close(); + } + + clientRef.current = null; + }; + + }, [appConfig.apiPath, getRoomData, token, waitingToReconnect, serverisRunningOnProcessorHardware]); /** * Send a status message to the server to get the current state of the room when the roomKey changes @@ -284,6 +350,7 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { sendSimpleMessage, addEventHandler, removeEventHandler, + reconnect, }} > {isConnected ? children : } From 9b06383a296b0a211edf382304c4db733e699f57 Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 22 May 2024 15:25:00 -0500 Subject: [PATCH 3/4] feat: add useOverflow & useScroll hooks --- src/lib/shared/hooks/index.ts | 2 ++ src/lib/shared/hooks/useOverflow.ts | 36 +++++++++++++++++++ src/lib/shared/hooks/useScroll.ts | 55 +++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+) create mode 100644 src/lib/shared/hooks/useOverflow.ts create mode 100644 src/lib/shared/hooks/useScroll.ts diff --git a/src/lib/shared/hooks/index.ts b/src/lib/shared/hooks/index.ts index 9f9e603..0148807 100644 --- a/src/lib/shared/hooks/index.ts +++ b/src/lib/shared/hooks/index.ts @@ -4,3 +4,5 @@ export * from "./useGetDeviceStateFromRoomConfiguration"; export * from "./usePressHoldRelease"; export * from "./useRoomIBasicVolumeWithFeedback"; export * from "./useTimeAndDate"; +export * from "./useOverflow"; +export * from "./useScroll"; \ No newline at end of file diff --git a/src/lib/shared/hooks/useOverflow.ts b/src/lib/shared/hooks/useOverflow.ts new file mode 100644 index 0000000..5c0fa53 --- /dev/null +++ b/src/lib/shared/hooks/useOverflow.ts @@ -0,0 +1,36 @@ +import { RefObject, useLayoutEffect, useState } from "react"; + +export function useOverflow(ref:RefObject, callback?:(hasOverflowVertical: boolean, hasOverflowHorizontal: boolean) => void):UseIsOverflowProps { + const [overflowHorizontal, setOverflowHorizontal] = useState(false); + const [overflowVertical, setOverflowVertical] = useState(false); + + + useLayoutEffect(() => { + const { current } = ref; + + const trigger = () => { + const hasOverflowVertical = current && current.scrollHeight > current.clientHeight; + const hasOverflowHorizontal = current && current.scrollWidth > current.clientWidth; + + setOverflowVertical(hasOverflowVertical ?? false); + setOverflowHorizontal(hasOverflowHorizontal ?? false); + + if (!callback) return; + + callback(hasOverflowVertical ?? false, hasOverflowHorizontal ?? false); + } + + if (!current) return; + + trigger(); + }, [ref, callback]) + + return { overflowHorizontal, overflowVertical }; +} + +export interface UseIsOverflowProps { + overflowHorizontal: boolean; + overflowVertical: boolean; +} + +export default useOverflow; \ No newline at end of file diff --git a/src/lib/shared/hooks/useScroll.ts b/src/lib/shared/hooks/useScroll.ts new file mode 100644 index 0000000..68830a5 --- /dev/null +++ b/src/lib/shared/hooks/useScroll.ts @@ -0,0 +1,55 @@ +import { RefObject, useLayoutEffect, useState } from "react"; + +export function useScroll(ref: RefObject): UseScrollProps { + + const [horizontalScrollPosition, setHorizontalScrollPosition] = useState(ref?.current?.scrollLeft ?? 0); + const [verticalScrollPosition, setVerticalScrollPosition] = useState(ref?.current?.scrollTop ?? 0); + + const scrollHorizontal = (increment: number) => { + const { current } = ref; + + if (!current) return; + + console.log(current.scrollLeft); + + current.scrollLeft += increment; + + console.log(current.scrollLeft); + } + + const scrollVertical = (increment: number) => { + const { current } = ref; + + if (!current) return; + + console.log(current.scrollTop); + + current.scrollTop += increment; + + console.log(current.scrollTop); + } + + useLayoutEffect(() => { + const { current } = ref; + + const trigger = () => { + setHorizontalScrollPosition(current?.scrollLeft ?? 0); + setVerticalScrollPosition(current?.scrollTop ?? 0); + } + + if (!current) return; + + trigger(); + }, [ref]) + + return {horizontalScrollPosition, verticalScrollPosition, scrollHorizontal, scrollVertical}; +} + +export default useScroll; + +export interface UseScrollProps { + scrollHorizontal: (increment: number) => void; + scrollVertical: (increment: number) => void; + horizontalScrollPosition: number; + verticalScrollPosition: number; +} \ No newline at end of file From 2a764a58266db54f3e060ac7d64a4130de889c0d Mon Sep 17 00:00:00 2001 From: Andrew Welker Date: Wed, 22 May 2024 15:46:58 -0500 Subject: [PATCH 4/4] fix: allow auto retries when using the direct server --- src/lib/utils/WebsocketProvider.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib/utils/WebsocketProvider.tsx b/src/lib/utils/WebsocketProvider.tsx index 85420b8..7e9e9f0 100644 --- a/src/lib/utils/WebsocketProvider.tsx +++ b/src/lib/utils/WebsocketProvider.tsx @@ -76,6 +76,10 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { catch (err) { console.log(err); + if(serverisRunningOnProcessorHardware){ + return true; + } + if (err instanceof AxiosError && err.response && err.response.status === 498) { console.error("Invalid token. Unable to join room"); store.dispatch(uiActions.setErrorMessage(`Token ${token} is invalid. Unable to join room`)); @@ -92,7 +96,7 @@ const WebsocketProvider = ({ children }: { children: ReactNode }) => { return false; } }, - [token] + [token, serverisRunningOnProcessorHardware] ); const reconnect = useCallback(() => {