diff --git a/apps/clients/chooser/src/App.tsx b/apps/clients/chooser/src/App.tsx index 3e786a4..f0d7c23 100644 --- a/apps/clients/chooser/src/App.tsx +++ b/apps/clients/chooser/src/App.tsx @@ -11,7 +11,7 @@ import superjson from "superjson"; import { ClientNotFound } from "@sora-vp/ui/client-not-found"; -import { KeyboardWebsocketProvider } from "./context/keyboard-websocket"; +import { HardwareWebsocketProvider } from "./context/hardware-websocket"; import { env } from "./env"; const router = createBrowserRouter([ @@ -78,13 +78,13 @@ export default function App() { return ( - + - + ); diff --git a/apps/clients/chooser/src/components/scanner/index.tsx b/apps/clients/chooser/src/components/scanner/index.tsx index b16bbf5..c4a0f13 100644 --- a/apps/clients/chooser/src/components/scanner/index.tsx +++ b/apps/clients/chooser/src/components/scanner/index.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from "react"; -import { useKeyboardWebsocket } from "@/context/keyboard-websocket"; +import { useHardwareWebsocket } from "@/context/hardware-websocket"; import { useParticipant } from "@/context/participant-context"; import { api } from "@/utils/api"; import { Navigate } from "react-router-dom"; @@ -10,24 +10,21 @@ import { MainScanner } from "./main-scanner"; export function ScannerComponent() { const { qrId, setQRCode } = useParticipant(); - const { wsEnabled, lastMessage, setLastMessage } = useKeyboardWebsocket(); + const { subscribe } = useHardwareWebsocket(); const [isQrInvalid, setInvalidQr] = useState(false); const participantAttended = api.clientConsumer.checkParticipantAttended.useMutation({ onSuccess() { - setLastMessage(null); - setQRCode(participantAttended.variables!); }, }); useEffect(() => { - if (wsEnabled && lastMessage) { - // Precheck before consuming command - if (lastMessage.startsWith("SORA-KEYBIND-")) { - const actualCommand = lastMessage.replace("SORA-KEYBIND-", ""); + const unsubHardware = subscribe((message) => { + if (message.startsWith("SORA-KEYBIND-")) { + const actualCommand = message.replace("SORA-KEYBIND-", ""); switch (actualCommand) { case "RELOAD": { @@ -37,8 +34,12 @@ export function ScannerComponent() { } } } - } - }, [isQrInvalid, participantAttended.isError, wsEnabled, lastMessage]); + }); + + return () => { + unsubHardware(); + }; + }, [isQrInvalid, participantAttended.isError]); const setIsQrValid = useCallback( (invalid: boolean) => setInvalidQr(invalid), diff --git a/apps/clients/chooser/src/context/keyboard-websocket.tsx b/apps/clients/chooser/src/context/hardware-websocket.tsx similarity index 54% rename from apps/clients/chooser/src/context/keyboard-websocket.tsx rename to apps/clients/chooser/src/context/hardware-websocket.tsx index 5823240..073aa2c 100644 --- a/apps/clients/chooser/src/context/keyboard-websocket.tsx +++ b/apps/clients/chooser/src/context/hardware-websocket.tsx @@ -3,8 +3,7 @@ import { useCallback, useContext, useEffect, - useMemo, - useState, + useRef, } from "react"; import { defaultWSPortAtom, enableWSConnectionAtom } from "@/utils/atom"; import { useAtomValue } from "jotai"; @@ -12,28 +11,31 @@ import useWebSocket, { ReadyState } from "react-use-websocket"; import { toast } from "@sora-vp/ui/toast"; -export interface IKeyboardWebsocket { +export type THardwareWebsocketCallback = (message: string) => void; + +export interface IHardwareWebsocket { wsEnabled: boolean; - lastMessage: string | null; - setLastMessage: (msg: string | null) => void; + subscribe(callbacK: THardwareWebsocketCallback): () => void; } -export const KeyboardWebsocketContext = createContext( - {} as IKeyboardWebsocket, +export const HardwareWebsocketContext = createContext( + {} as IHardwareWebsocket, ); -export const KeyboardWebsocketProvider = ({ +export const HardwareWebsocketProvider = ({ children, }: { children: React.ReactNode; }) => { - const [lastMessage, setLastMessageState] = - useState(null); + const currentSubscriberIdRef = useRef(0); + const subscribersRef = useRef>( + new Map(), + ); const wsPortNumber = useAtomValue(defaultWSPortAtom); const wsEnabled = useAtomValue(enableWSConnectionAtom); - const { lastMessage: libLastMessage, readyState } = useWebSocket( + const { lastMessage, readyState } = useWebSocket( wsEnabled ? `ws://127.0.0.1:${wsPortNumber}/ws` : null, { share: true, @@ -47,23 +49,23 @@ export const KeyboardWebsocketProvider = ({ }, ); - const setLastMessage = useCallback( - (msg: IKeyboardWebsocket["lastMessage"]) => setLastMessageState(msg), - [], - ); + const subscribe = useCallback((callback: THardwareWebsocketCallback) => { + const id = currentSubscriberIdRef.current; + subscribersRef.current.set(id, callback); + currentSubscriberIdRef.current++; - const contextValue = useMemo( - () => ({ - wsEnabled, - lastMessage, - setLastMessage, - }), - [wsEnabled, lastMessage], - ); + return () => { + subscribersRef.current.delete(id); + }; + }, []); useEffect(() => { - if (libLastMessage) setLastMessageState(libLastMessage.data); - }, [libLastMessage]); + if (lastMessage) { + Array.from(subscribersRef.current).forEach(([, callback]) => { + callback(lastMessage.data); + }); + } + }, [lastMessage]); useEffect(() => { if (wsEnabled) { @@ -96,11 +98,16 @@ export const KeyboardWebsocketProvider = ({ }, [readyState, wsEnabled]); return ( - + {children} - + ); }; -export const useKeyboardWebsocket = () => - useContext(KeyboardWebsocketContext) as IKeyboardWebsocket; +export const useHardwareWebsocket = () => + useContext(HardwareWebsocketContext) as IHardwareWebsocket; diff --git a/apps/clients/chooser/src/context/participant-context.tsx b/apps/clients/chooser/src/context/participant-context.tsx index 05acd48..6fcf624 100644 --- a/apps/clients/chooser/src/context/participant-context.tsx +++ b/apps/clients/chooser/src/context/participant-context.tsx @@ -11,7 +11,7 @@ import { api } from "@/utils/api"; import { motion } from "framer-motion"; import { Navigate } from "react-router-dom"; -import { useKeyboardWebsocket } from "./keyboard-websocket"; +import { useHardwareWebsocket } from "./hardware-websocket"; export interface IParticipantContext { name: string | null; @@ -30,7 +30,7 @@ export const ParticipantProvider = ({ }: { children: React.ReactNode; }) => { - const { wsEnabled, lastMessage } = useKeyboardWebsocket(); + const { subscribe } = useHardwareWebsocket(); const [qrId, setQrId] = useState(null); const [votedSuccessfully, setVoted] = useState(false); @@ -50,10 +50,9 @@ export const ParticipantProvider = ({ ); useEffect(() => { - if (wsEnabled && lastMessage) { - // Precheck before consuming command - if (lastMessage.startsWith("SORA-KEYBIND-")) { - const actualCommand = lastMessage.replace("SORA-KEYBIND-", ""); + const unsubHardware = subscribe((message) => { + if (message.startsWith("SORA-KEYBIND-")) { + const actualCommand = message.replace("SORA-KEYBIND-", ""); switch (actualCommand) { case "RELOAD": { @@ -69,14 +68,12 @@ export const ParticipantProvider = ({ } } } - } - }, [ - qrId, - participantQuery.isFetched, - participantQuery.data, - wsEnabled, - lastMessage, - ]); + + return () => { + unsubHardware(); + }; + }); + }, [qrId, participantQuery.isFetched, participantQuery.data]); const propsValue = useMemo(() => { if (!qrId) diff --git a/apps/clients/chooser/src/context/server-setting.tsx b/apps/clients/chooser/src/context/server-setting.tsx index 45f06a3..a0632f6 100644 --- a/apps/clients/chooser/src/context/server-setting.tsx +++ b/apps/clients/chooser/src/context/server-setting.tsx @@ -4,7 +4,7 @@ import { api } from "@/utils/api"; import { motion } from "framer-motion"; import { Loader } from "lucide-react"; -import { useKeyboardWebsocket } from "./keyboard-websocket"; +import { useHardwareWebsocket } from "./hardware-websocket"; import { useParticipant } from "./participant-context"; interface ISettingContext { @@ -20,7 +20,7 @@ export const ServerSettingProvider = ({ }: { children: React.ReactNode; }) => { - const { wsEnabled, lastMessage } = useKeyboardWebsocket(); + const { subscribe } = useHardwareWebsocket(); const { qrId, setQRCode } = useParticipant(); const [errorMessage, setErrorMessage] = useState(""); @@ -31,10 +31,9 @@ export const ServerSettingProvider = ({ }); useEffect(() => { - if (wsEnabled && lastMessage) { - // Precheck before consuming command - if (lastMessage.startsWith("SORA-KEYBIND-")) { - const actualCommand = lastMessage.replace("SORA-KEYBIND-", ""); + const unsubHardware = subscribe((message) => { + if (message.startsWith("SORA-KEYBIND-")) { + const actualCommand = message.replace("SORA-KEYBIND-", ""); switch (actualCommand) { case "RELOAD": { @@ -44,8 +43,12 @@ export const ServerSettingProvider = ({ } } } - } - }, [settingsQuery.errorUpdateCount, wsEnabled, lastMessage]); + }); + + return () => { + unsubHardware(); + }; + }, [settingsQuery.errorUpdateCount]); useEffect(() => { if (settingsQuery.error) setErrorMessage(settingsQuery.error.message); diff --git a/apps/clients/chooser/src/routes/vote-page.tsx b/apps/clients/chooser/src/routes/vote-page.tsx index c4c26f8..d32a8eb 100644 --- a/apps/clients/chooser/src/routes/vote-page.tsx +++ b/apps/clients/chooser/src/routes/vote-page.tsx @@ -1,6 +1,6 @@ import { useCallback, useEffect, useMemo, useState } from "react"; import { UniversalError } from "@/components/universal-error"; -import { useKeyboardWebsocket } from "@/context/keyboard-websocket"; +import { useHardwareWebsocket } from "@/context/hardware-websocket"; import { ensureQRIDExist, useParticipant } from "@/context/participant-context"; import { env } from "@/env"; import { api } from "@/utils/api"; @@ -91,7 +91,7 @@ const CurrentParticipantInfo = (props: { isSuccess?: boolean }) => { function VotePage() { const { qrId, setQRCode, setVotedSuccessfully } = useParticipant(); - const { wsEnabled, lastMessage, setLastMessage } = useKeyboardWebsocket(); + const { subscribe } = useHardwareWebsocket(); const successTimeout = useAtomValue(successTimeoutAtom); @@ -116,9 +116,6 @@ function VotePage() { setAlertOpen(false); setID(null); }, - onSettled() { - setLastMessage(null); - }, }); const candidateName = useMemo( @@ -141,8 +138,6 @@ function VotePage() { const chooseCandidate = useCallback(() => { if (!cannotPushKey) { if (qrId && currentID && alertOpen) { - setLastMessage(null); - upvoteCandidate.mutate({ id: currentID, qrId, @@ -176,7 +171,6 @@ function VotePage() { if (!upvoteCandidate.isPending) { setID(null); setAlertOpen(false); - setLastMessage(null); } break; @@ -228,17 +222,15 @@ function VotePage() { }, [upvoteCandidate.isPending, alertOpen, triggerOpen, chooseCandidate]); useEffect(() => { - if (wsEnabled && lastMessage) { - // Precheck before consuming command - if (lastMessage.startsWith("SORA-KEYBIND-")) { - const actualCommand = lastMessage.replace("SORA-KEYBIND-", ""); + const unsubHardware = subscribe((message) => { + if (message.startsWith("SORA-KEYBIND-")) { + const actualCommand = message.replace("SORA-KEYBIND-", ""); switch (actualCommand) { case "ESC": { if (!upvoteCandidate.isPending) { setID(null); setAlertOpen(false); - setLastMessage(null); } break; @@ -288,15 +280,17 @@ function VotePage() { } } } - } + }); + + return () => { + unsubHardware(); + }; }, [ upvoteCandidate.isPending, candidateList.errorUpdateCount, alertOpen, triggerOpen, chooseCandidate, - wsEnabled, - lastMessage, ]); useEffect(() => { @@ -443,7 +437,6 @@ function VotePage() { open={alertOpen || (upvoteCandidate.isPending && !!qrId)} onOpenChange={() => { setAlertOpen((prev) => !prev); - setLastMessage(null); }} >