diff --git a/frontend/src/contexts/auth.context.tsx b/frontend/src/contexts/auth.context.tsx index eca0700f..030c4269 100644 --- a/frontend/src/contexts/auth.context.tsx +++ b/frontend/src/contexts/auth.context.tsx @@ -101,21 +101,18 @@ export const AuthProvider = ({ children }: AuthProviderProps): JSX.Element => { setUser(user); }; const isAuthenticated = !!user; + const { subscribe } = useBroadcastChannel(); useEffect(() => { const search = location.search; setSearch(search); setIsReady(true); - }, []); - const { subscribe } = useBroadcastChannel(); - - subscribe((message) => { - if (message.data === "logout") { + subscribe("logout", () => { router.reload(); - } - }); + }); + }, []); if (!isReady || isLoading) return ; diff --git a/frontend/src/contexts/broadcast-channel.context.tsx b/frontend/src/contexts/broadcast-channel.context.tsx index e6b8e588..5363ab18 100644 --- a/frontend/src/contexts/broadcast-channel.context.tsx +++ b/frontend/src/contexts/broadcast-channel.context.tsx @@ -12,98 +12,123 @@ import { ReactNode, useContext, useEffect, - useMemo, useRef, } from "react"; -import { useTabUuid } from "@/hooks/useTabUuid"; +import { generateId } from "@/utils/generateId"; + +export enum EBCEvent { + LOGOUT = "logout", +} + +type BroadcastChannelPayload = { + event: `${EBCEvent}`; + data?: string | number | boolean | Record | undefined | null; +}; export type BroadcastChannelData = { tabId: string; - data: string | number | boolean | Record | undefined | null; + payload: BroadcastChannelPayload; }; export interface IBroadcastChannelProps { channelName: string; - children?: ReactNode; + children: ReactNode; } -export const BroadcastChannelContext = createContext<{ - subscribers: ((message: BroadcastChannelData) => void)[]; - subscribe: (callback: (message: BroadcastChannelData) => void) => () => void; - postMessage: (message: BroadcastChannelData) => void; -}>({ - subscribers: [], +const getOrCreateTabId = () => { + let storedTabId = sessionStorage.getItem("tab_uuid"); + + if (storedTabId) { + return storedTabId; + } + + storedTabId = generateId(); + sessionStorage.setItem("tab_uuid", storedTabId); + + return storedTabId; +}; + +interface IBroadcastChannelContext { + subscribe: ( + event: `${EBCEvent}`, + callback: (message: BroadcastChannelData) => void, + ) => () => void; + postMessage: (payload: BroadcastChannelPayload) => void; +} + +export const BroadcastChannelContext = createContext({ subscribe: () => () => {}, postMessage: () => {}, }); export const BroadcastChannelProvider: FC = ({ - channelName, children, + channelName, }) => { - const channelRef = useRef(null); - const subscribersRef = useRef void>>( - [], + const channelRef = useRef( + new BroadcastChannel(channelName), ); - const tabUuidRef = useTabUuid(); + const subscribersRef = useRef< + Record void>> + >({}); + const tabUuid = getOrCreateTabId(); useEffect(() => { - const channel = new BroadcastChannel(channelName); + const handleMessage = ({ data }: MessageEvent) => { + const { tabId, payload } = data; - channelRef.current = channel; + if (tabId === tabUuid) { + return; + } - const handleMessage = (event: MessageEvent) => { - const { tabId, data } = event.data; - - if (tabId === tabUuidRef.current) return; - - subscribersRef.current.forEach((callback) => callback(data)); + subscribersRef.current[payload.event]?.forEach((callback) => + callback(data), + ); }; - channel.addEventListener("message", handleMessage); + channelRef.current.addEventListener("message", handleMessage); return () => { - channel.removeEventListener("message", handleMessage); - channel.close(); + channelRef.current.removeEventListener("message", handleMessage); + channelRef.current.close(); }; - }, [channelName, tabUuidRef]); + }, []); - const postMessage = (message: BroadcastChannelData) => { - channelRef.current?.postMessage({ - tabId: tabUuidRef.current, - data: message, - }); - }; - const subscribe = (callback: (message: BroadcastChannelData) => void) => { - subscribersRef.current.push(callback); + const subscribe: IBroadcastChannelContext["subscribe"] = ( + event, + callback, + ) => { + subscribersRef.current[event] ??= []; + subscribersRef.current[event]?.push(callback); return () => { - const index = subscribersRef.current.indexOf(callback); + const index = subscribersRef.current[event]?.indexOf(callback) ?? -1; if (index !== -1) { - subscribersRef.current.splice(index, 1); + subscribersRef.current[event]?.splice(index, 1); } }; }; - const contextValue = useMemo( - () => ({ - subscribers: subscribersRef.current, - subscribe, - postMessage, - }), - [], - ); + const postMessage: IBroadcastChannelContext["postMessage"] = (payload) => { + channelRef.current?.postMessage({ + tabId: tabUuid, + payload, + }); + }; return ( - + {children} ); }; -export default BroadcastChannelProvider; - export const useBroadcastChannel = () => { const context = useContext(BroadcastChannelContext); @@ -115,3 +140,5 @@ export const useBroadcastChannel = () => { return context; }; + +export default BroadcastChannelProvider; diff --git a/frontend/src/hooks/entities/auth-hooks.ts b/frontend/src/hooks/entities/auth-hooks.ts index 105718c9..e6df15b3 100755 --- a/frontend/src/hooks/entities/auth-hooks.ts +++ b/frontend/src/hooks/entities/auth-hooks.ts @@ -23,7 +23,6 @@ import { useSocket } from "@/websocket/socket-hooks"; import { useFind } from "../crud/useFind"; import { useApiClient } from "../useApiClient"; import { CURRENT_USER_KEY, useAuth, useLogoutRedirection } from "../useAuth"; -import { useTabUuid } from "../useTabUuid"; import { useToast } from "../useToast"; import { useTranslate } from "../useTranslate"; @@ -61,7 +60,6 @@ export const useLogout = ( const { toast } = useToast(); const { t } = useTranslate(); const { postMessage } = useBroadcastChannel(); - const uuid = useTabUuid(); return useMutation({ ...options, @@ -72,7 +70,7 @@ export const useLogout = ( }, onSuccess: async () => { queryClient.removeQueries([CURRENT_USER_KEY]); - postMessage({ data: "logout", tabId: uuid.current || "" }); + postMessage({ event: "logout" }); await logoutRedirection(); toast.success(t("message.logout_success")); }, diff --git a/frontend/src/hooks/useTabUuid.ts b/frontend/src/hooks/useTabUuid.ts deleted file mode 100644 index 5f86619f..00000000 --- a/frontend/src/hooks/useTabUuid.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { useRef } from "react"; - -import { generateId } from "@/utils/generateId"; - -const getOrCreateTabId = () => { - let storedTabId = sessionStorage.getItem("tab_uuid"); - - if (storedTabId) { - return storedTabId; - } - - storedTabId = generateId(); - sessionStorage.setItem("tab_uuid", storedTabId); - - return storedTabId; -}; - -export const useTabUuid = () => { - const tabUuidRef = useRef(getOrCreateTabId()); - - return tabUuidRef; -}; diff --git a/widget/src/hooks/useTabUuid.ts b/widget/src/hooks/useTabUuid.ts deleted file mode 100644 index 3f0b4018..00000000 --- a/widget/src/hooks/useTabUuid.ts +++ /dev/null @@ -1,30 +0,0 @@ -/* - * 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 { useRef } from "react"; - -import { generateId } from "../utils/generateId"; - -const getOrCreateTabId = () => { - let storedTabId = sessionStorage.getItem("tab_uuid"); - - if (storedTabId) { - return storedTabId; - } - - storedTabId = generateId(); - sessionStorage.setItem("tab_uuid", storedTabId); - - return storedTabId; -}; - -export const useTabUuid = () => { - const tabUuidRef = useRef(getOrCreateTabId()); - - return tabUuidRef; -}; diff --git a/widget/src/providers/BroadcastChannelProvider.tsx b/widget/src/providers/BroadcastChannelProvider.tsx index f4e23d03..b900c1df 100644 --- a/widget/src/providers/BroadcastChannelProvider.tsx +++ b/widget/src/providers/BroadcastChannelProvider.tsx @@ -12,99 +12,125 @@ import { ReactNode, useContext, useEffect, - useMemo, useRef, } from "react"; -import { useTabUuid } from "../hooks/useTabUuid"; +import { generateId } from "../utils/generateId"; + +export enum EBCEvent { + LOGOUT = "logout", +} + +type BroadcastChannelPayload = { + event: `${EBCEvent}`; + data?: string | number | boolean | Record | undefined | null; +}; export type BroadcastChannelData = { tabId: string; - data: string | number | boolean | Record | undefined | null; + payload: BroadcastChannelPayload; }; export interface IBroadcastChannelProps { channelName: string; - children?: ReactNode; + children: ReactNode; } -export const BroadcastChannelContext = createContext<{ - subscribers: ((message: BroadcastChannelData) => void)[]; - subscribe: (callback: (message: BroadcastChannelData) => void) => () => void; - postMessage: (message: BroadcastChannelData) => void; -}>({ - subscribers: [], +const getOrCreateTabId = () => { + let storedTabId = sessionStorage.getItem("tab_uuid"); + + if (storedTabId) { + return storedTabId; + } + + storedTabId = generateId(); + sessionStorage.setItem("tab_uuid", storedTabId); + + return storedTabId; +}; + +interface IBroadcastChannelContext { + subscribe: ( + event: `${EBCEvent}`, + callback: (message: BroadcastChannelData) => void, + ) => () => void; + postMessage: (payload: BroadcastChannelPayload) => void; +} + +export const BroadcastChannelContext = createContext({ subscribe: () => () => {}, postMessage: () => {}, }); export const BroadcastChannelProvider: FC = ({ - channelName, children, + channelName, }) => { - const channelRef = useRef(null); - const subscribersRef = useRef void>>( - [], + const channelRef = useRef( + new BroadcastChannel(channelName), ); - const tabUuidRef = useTabUuid(); + const subscribersRef = useRef< + Record void>> + >({}); + const tabUuid = getOrCreateTabId(); useEffect(() => { - const channel = new BroadcastChannel(channelName); + const handleMessage = ({ data }: MessageEvent) => { + const { tabId, payload } = data; - channelRef.current = channel; + if (tabId === tabUuid) { + return; + } - const handleMessage = (event: MessageEvent) => { - const { tabId, data } = event.data; - - if (tabId === tabUuidRef.current) return; - - subscribersRef.current.forEach((callback) => callback(data)); + subscribersRef.current[payload.event]?.forEach((callback) => + callback(data), + ); }; - channel.addEventListener("message", handleMessage); + channelRef.current.addEventListener("message", handleMessage); return () => { - channel.removeEventListener("message", handleMessage); - channel.close(); + channelRef.current.removeEventListener("message", handleMessage); + // eslint-disable-next-line react-hooks/exhaustive-deps + channelRef.current.close(); }; - }, [channelName, tabUuidRef]); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, []); - const postMessage = (message: BroadcastChannelData) => { - channelRef.current?.postMessage({ - tabId: tabUuidRef.current, - data: message, - }); - }; - const subscribe = (callback: (message: BroadcastChannelData) => void) => { - subscribersRef.current.push(callback); + const subscribe: IBroadcastChannelContext["subscribe"] = ( + event, + callback, + ) => { + subscribersRef.current[event] ??= []; + subscribersRef.current[event]?.push(callback); return () => { - const index = subscribersRef.current.indexOf(callback); + const index = subscribersRef.current[event]?.indexOf(callback) ?? -1; if (index !== -1) { - subscribersRef.current.splice(index, 1); + subscribersRef.current[event]?.splice(index, 1); } }; }; - const contextValue = useMemo( - () => ({ - subscribers: subscribersRef.current, - subscribe, - postMessage, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [], - ); + const postMessage: IBroadcastChannelContext["postMessage"] = (payload) => { + channelRef.current?.postMessage({ + tabId: tabUuid, + payload, + }); + }; return ( - + {children} ); }; -export default BroadcastChannelProvider; - export const useBroadcastChannel = () => { const context = useContext(BroadcastChannelContext); @@ -116,3 +142,5 @@ export const useBroadcastChannel = () => { return context; }; + +export default BroadcastChannelProvider; diff --git a/widget/src/providers/ChatProvider.tsx b/widget/src/providers/ChatProvider.tsx index 9c4b952f..9a8ba246 100644 --- a/widget/src/providers/ChatProvider.tsx +++ b/widget/src/providers/ChatProvider.tsx @@ -385,6 +385,8 @@ const ChatProvider: React.FC<{ } }, [syncState, isOpen]); + const { subscribe } = useBroadcastChannel(); + useEffect(() => { if (screen === "chat" && connectionState === ConnectionState.connected) { handleSubscription(); @@ -404,6 +406,10 @@ const ChatProvider: React.FC<{ socketCtx.socket.io.on("reconnect", reSubscribe); + subscribe("logout", () => { + socketCtx.socket.disconnect(); + }); + return () => { socketCtx.socket.io.off("reconnect", reSubscribe); }; @@ -452,13 +458,6 @@ const ChatProvider: React.FC<{ setMessage, handleSubscription, }; - const { subscribe } = useBroadcastChannel(); - - subscribe(({ data }) => { - if (data === "logout") { - socketCtx.socket.disconnect(); - } - }); return ( {children}