From 09ec35f520eb3dacb2e2dee3dfe3f73e28d15f33 Mon Sep 17 00:00:00 2001 From: yassinedorbozgithub Date: Wed, 29 Jan 2025 16:47:55 +0100 Subject: [PATCH] fix: add brodcastChannel HOC --- frontend/src/contexts/auth.context.tsx | 6 +- frontend/src/contexts/broadcast-channel.tsx | 48 ++++++++++++++ frontend/src/hooks/entities/auth-hooks.ts | 3 +- ...castChannel.ts => useBroadcastChannel.tsx} | 64 +++++++++---------- frontend/src/pages/_app.tsx | 21 +++--- 5 files changed, 94 insertions(+), 48 deletions(-) create mode 100644 frontend/src/contexts/broadcast-channel.tsx rename frontend/src/hooks/{useBroadcastChannel.ts => useBroadcastChannel.tsx} (63%) diff --git a/frontend/src/contexts/auth.context.tsx b/frontend/src/contexts/auth.context.tsx index 7fac0e86..e2ac9227 100644 --- a/frontend/src/contexts/auth.context.tsx +++ b/frontend/src/contexts/auth.context.tsx @@ -110,10 +110,10 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { }, []); const tabUuidRef = useTabUuid(); - const tabUuid = tabUuidRef.current; + const { useBroadcast } = useBroadcastChannel(); - useBroadcastChannel("session", (e) => { - if (e.data.value === "logout" && e.data.uuid !== tabUuid) { + useBroadcast("session", (e) => { + if (e.data.value === "logout" && e.data.uuid !== tabUuidRef.current) { router.reload(); } }); diff --git a/frontend/src/contexts/broadcast-channel.tsx b/frontend/src/contexts/broadcast-channel.tsx new file mode 100644 index 00000000..b11fd2df --- /dev/null +++ b/frontend/src/contexts/broadcast-channel.tsx @@ -0,0 +1,48 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { createContext } from "react"; + +import { useBroadcast } from "@/hooks/useBroadcastChannel"; + +export interface IBroadcastChannelContext { + useBroadcast: ( + channelName: string, + handleMessage?: (event: MessageEvent) => void, + handleMessageError?: (event: MessageEvent) => void, + ) => (data: BroadcastChannelData) => void; +} + +export type BroadcastChannelData = { + uuid: string; + value: string | number | boolean | Record | undefined | null; +}; + +export interface IBroadcastChannelProps { + children?: React.ReactNode; +} + +export const BroadcastChannelContext = createContext({ + useBroadcast: () => () => {}, +}); + +export const BroadcastChannelProvider: React.FC = ({ + children, +}) => { + return ( + + {children} + + ); +}; + +export default BroadcastChannelProvider; diff --git a/frontend/src/hooks/entities/auth-hooks.ts b/frontend/src/hooks/entities/auth-hooks.ts index 60a69094..f3687549 100755 --- a/frontend/src/hooks/entities/auth-hooks.ts +++ b/frontend/src/hooks/entities/auth-hooks.ts @@ -60,7 +60,8 @@ export const useLogout = ( const { logoutRedirection } = useLogoutRedirection(); const { toast } = useToast(); const { t } = useTranslate(); - const broadcastLogoutAcrossTabs = useBroadcastChannel("session"); + const { useBroadcast } = useBroadcastChannel(); + const broadcastLogoutAcrossTabs = useBroadcast("session"); const tabUuidRef = useTabUuid(); const tabUuid = tabUuidRef.current; diff --git a/frontend/src/hooks/useBroadcastChannel.ts b/frontend/src/hooks/useBroadcastChannel.tsx similarity index 63% rename from frontend/src/hooks/useBroadcastChannel.ts rename to frontend/src/hooks/useBroadcastChannel.tsx index 4d4417a2..b933ca53 100644 --- a/frontend/src/hooks/useBroadcastChannel.ts +++ b/frontend/src/hooks/useBroadcastChannel.tsx @@ -1,5 +1,5 @@ /* - * Copyright © 2025 Hexastack. All rights reserved. + * Copyright © 2024 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. @@ -7,27 +7,21 @@ */ import * as React from "react"; +import { useContext } from "react"; -export type BroadcastChannelData = { - uuid: string; - value: string | number | boolean | Record | undefined | null; -}; +import { + BroadcastChannelContext, + BroadcastChannelData, + IBroadcastChannelContext, +} from "@/contexts/broadcast-channel"; -/** - * React hook to create and manage a Broadcast Channel across multiple browser windows. - * - * @param channelName Static name of channel used across the browser windows. - * @param handleMessage Callback to handle the event generated when `message` is received. - * @param handleMessageError [optional] Callback to handle the event generated when `error` is received. - * @returns A function to send/post message on the channel. - */ -export function useBroadcastChannel< +export const useBroadcast = < T extends BroadcastChannelData = BroadcastChannelData, >( channelName: string, handleMessage?: (event: MessageEvent) => void, handleMessageError?: (event: MessageEvent) => void, -): (data: T) => void { +): ((data: T) => void) => { const channelRef = React.useRef( typeof window !== "undefined" && "BroadcastChannel" in window ? new BroadcastChannel(channelName + "-channel") @@ -42,26 +36,17 @@ export function useBroadcastChannel< ); return (data: T) => channelRef.current?.postMessage(data); -} +}; -/** - * React hook to manage state across browser windows. Has the similar signature as `React.useState`. - * - * @param channelName Static name of channel used across the browser windows. - * @param initialState Initial state. - * @returns Tuple of state and setter for the state. - */ -export function useBroadcastState< +export const useBroadcastState = < T extends BroadcastChannelData = BroadcastChannelData, >( channelName: string, initialState: T, -): [T, React.Dispatch>, boolean] { +): [T, React.Dispatch>, boolean] => { const [isPending, startTransition] = React.useTransition(); const [state, setState] = React.useState(initialState); - const broadcast = useBroadcastChannel(channelName, (ev) => - setState(ev.data), - ); + const broadcast = useBroadcast(channelName, (ev) => setState(ev.data)); const updateState: React.Dispatch> = React.useCallback( (input) => { @@ -77,16 +62,13 @@ export function useBroadcastState< ); return [state, updateState, isPending]; -} +}; -// Helpers - -/** Hook to subscribe/unsubscribe from channel events. */ -function useChannelEventListener( +const useChannelEventListener = ( channel: BroadcastChannel | null, event: K, handler?: (e: BroadcastChannelEventMap[K]) => void, -) { +) => { const callbackRef = React.useRef(handler); if (callbackRef.current !== handler) { @@ -107,4 +89,16 @@ function useChannelEventListener( channel.removeEventListener(event, callback); }; }, [channel, event]); -} +}; + +export const useBroadcastChannel = (): IBroadcastChannelContext => { + const context = useContext(BroadcastChannelContext); + + if (!context) { + throw new Error( + "useBroadcastChannel must be used within an BroadcastChannelContext", + ); + } + + return context; +}; diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx index a0b2067d..4f69a198 100644 --- a/frontend/src/pages/_app.tsx +++ b/frontend/src/pages/_app.tsx @@ -19,6 +19,7 @@ import { ReactQueryDevtools } from "react-query/devtools"; import { SnackbarCloseButton } from "@/app-components/displays/Toast/CloseButton"; import { ApiClientProvider } from "@/contexts/apiClient.context"; import { AuthProvider } from "@/contexts/auth.context"; +import BroadcastChannelProvider from "@/contexts/broadcast-channel"; import { ConfigProvider } from "@/contexts/config.context"; import { PermissionProvider } from "@/contexts/permission.context"; import { SettingsProvider } from "@/contexts/setting.context"; @@ -83,15 +84,17 @@ const App = ({ Component, pageProps }: TAppPropsWithLayout) => { - - - - - {getLayout()} - - - - + + + + + + {getLayout()} + + + + +