diff --git a/frontend/src/contexts/broadcast-channel.tsx b/frontend/src/contexts/broadcast-channel.context.tsx
similarity index 100%
rename from frontend/src/contexts/broadcast-channel.tsx
rename to frontend/src/contexts/broadcast-channel.context.tsx
diff --git a/frontend/src/hooks/useBroadcastChannel.tsx b/frontend/src/hooks/useBroadcastChannel.ts
similarity index 98%
rename from frontend/src/hooks/useBroadcastChannel.tsx
rename to frontend/src/hooks/useBroadcastChannel.ts
index b933ca53..81ec012c 100644
--- a/frontend/src/hooks/useBroadcastChannel.tsx
+++ b/frontend/src/hooks/useBroadcastChannel.ts
@@ -13,7 +13,7 @@ import {
BroadcastChannelContext,
BroadcastChannelData,
IBroadcastChannelContext,
-} from "@/contexts/broadcast-channel";
+} from "@/contexts/broadcast-channel.context";
export const useBroadcast = <
T extends BroadcastChannelData = BroadcastChannelData,
diff --git a/frontend/src/pages/_app.tsx b/frontend/src/pages/_app.tsx
index 4f69a198..21c31c6e 100644
--- a/frontend/src/pages/_app.tsx
+++ b/frontend/src/pages/_app.tsx
@@ -19,7 +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 BroadcastChannelProvider from "@/contexts/broadcast-channel.context";
import { ConfigProvider } from "@/contexts/config.context";
import { PermissionProvider } from "@/contexts/permission.context";
import { SettingsProvider } from "@/contexts/setting.context";
diff --git a/widget/src/UiChatWidget.tsx b/widget/src/UiChatWidget.tsx
index 96dc5de4..0e636a19 100644
--- a/widget/src/UiChatWidget.tsx
+++ b/widget/src/UiChatWidget.tsx
@@ -10,6 +10,7 @@ import { PropsWithChildren } from "react";
import Launcher from "./components/Launcher";
import UserSubscription from "./components/UserSubscription";
+import BroadcastChannelProvider from "./providers/BroadcastChannelProvider";
import ChatProvider from "./providers/ChatProvider";
import { ColorProvider } from "./providers/ColorProvider";
import { ConfigProvider } from "./providers/ConfigProvider";
@@ -41,17 +42,19 @@ function UiChatWidget({
-
-
-
-
-
+
+
+
+
+
+
+
diff --git a/widget/src/hooks/useBroadcastChannel.ts b/widget/src/hooks/useBroadcastChannel.ts
index 4d4417a2..d23578e1 100644
--- a/widget/src/hooks/useBroadcastChannel.ts
+++ b/widget/src/hooks/useBroadcastChannel.ts
@@ -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 "../providers/BroadcastChannelProvider";
-/**
- * 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/widget/src/providers/BroadcastChannelProvider.tsx b/widget/src/providers/BroadcastChannelProvider.tsx
new file mode 100644
index 00000000..ba323639
--- /dev/null
+++ b/widget/src/providers/BroadcastChannelProvider.tsx
@@ -0,0 +1,49 @@
+/*
+ * 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 = ({
+ // eslint-disable-next-line react/prop-types
+ children,
+}) => {
+ return (
+
+ {children}
+
+ );
+};
+
+export default BroadcastChannelProvider;
diff --git a/widget/src/providers/ChatProvider.tsx b/widget/src/providers/ChatProvider.tsx
index cf05da4f..ff408791 100644
--- a/widget/src/providers/ChatProvider.tsx
+++ b/widget/src/providers/ChatProvider.tsx
@@ -454,10 +454,10 @@ const ChatProvider: React.FC<{
handleSubscription,
};
const tabUuidRef = useTabUuid();
- const tabUuid = tabUuidRef.current;
+ const { useBroadcast } = useBroadcastChannel();
- useBroadcastChannel("session", ({ data }) => {
- if (data.value === "logout" && data.uuid !== tabUuid) {
+ useBroadcast("session", ({ data }) => {
+ if (data.value === "logout" && data.uuid !== tabUuidRef.current) {
socketCtx.socket.disconnect();
}
});