diff --git a/widget/src/hooks/useBroadcastChannel.ts b/widget/src/hooks/useBroadcastChannel.ts index beba882d..7caaa69d 100644 --- a/widget/src/hooks/useBroadcastChannel.ts +++ b/widget/src/hooks/useBroadcastChannel.ts @@ -6,52 +6,136 @@ * 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 { useEffect, useRef, useState } from "react"; +import * as React from "react"; -export enum ETabMode { - PRIMARY = "primary", - SECONDARY = "secondary", -} +export type BroadcastChannelData = + | string + | number + | boolean + | Record + | undefined + | null; -export enum EBCEvent { - LOGOUT_END_SESSION = "logout-end-session", -} +/** + * 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. + * @example + * ```tsx + * import {useBroadcastChannel} from 'react-broadcast-channel'; + * + * function App () { + * const postUserIdMessage = useBroadcastChannel('userId', (e) => alert(e.data)); + * return (); + * } + * ``` + * --- + * Works in browser that support Broadcast Channel API natively. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API#browser_compatibility). + * To support other browsers, install and use [broadcastchannel-polyfill](https://www.npmjs.com/package/broadcastchannel-polyfill). + */ +export function useBroadcastChannel( + channelName: string, + handleMessage?: (event: MessageEvent) => void, + handleMessageError?: (event: MessageEvent) => void, +): (data: T) => void { + const channelRef = React.useRef(null); -export const useBroadcastChannel = ( - channelName: string = "main-broadcast-channel", - initialValue?: EBCEvent, -) => { - const channelRef = useRef(null); - const [value, setValue] = useState(initialValue); - const [mode, setMode] = useState(ETabMode.PRIMARY); - - useEffect(() => { - channelRef.current = new BroadcastChannel(channelName); + React.useEffect(() => { + if (typeof window !== "undefined" && "BroadcastChannel" in window) { + channelRef.current = new BroadcastChannel(channelName + "-channel"); + } }, [channelName]); - useEffect(() => { - if (channelRef.current) { - channelRef.current.addEventListener("message", (event) => { - if (mode === ETabMode.PRIMARY) { - setValue(event.data); - setMode(ETabMode.SECONDARY); - } - }); - channelRef.current.postMessage(initialValue); + useChannelEventListener(channelRef.current, "message", handleMessage); + useChannelEventListener( + channelRef.current, + "messageerror", + handleMessageError, + ); + + return React.useCallback( + (data: T) => channelRef.current?.postMessage(data), + [channelRef.current], + ); +} + +/** + * 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. + * @example + * ```tsx + * import {useBroadcastState} from 'react-broadcast-channel'; + * + * function App () { + * const [count, setCount] = useBroadcastState('count', 0); + * return ( + *
+ * + * {count} + * + *
+ * ); + * } + * ``` + * --- + * Works in browser that support Broadcast Channel API natively. See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Broadcast_Channel_API#browser_compatibility). + * To support other browsers, install and use [broadcastchannel-polyfill](https://www.npmjs.com/package/broadcastchannel-polyfill). + */ +export function useBroadcastState( + channelName: string, + initialState: T, +): [T, React.Dispatch>, boolean] { + const [isPending, startTransition] = React.useTransition(); + const [state, setState] = React.useState(initialState); + const broadcast = useBroadcastChannel(channelName, (ev) => + setState(ev.data), + ); + const updateState: React.Dispatch> = + React.useCallback( + (input) => { + setState((prev) => { + const newState = typeof input === "function" ? input(prev) : input; + + startTransition(() => broadcast(newState)); + + return newState; + }); + }, + [broadcast], + ); + + return [state, updateState, isPending]; +} + +// Helpers + +/** Hook to subscribe/unsubscribe from channel events. */ +function useChannelEventListener( + channel: BroadcastChannel | null, + event: K, + handler?: (e: BroadcastChannelEventMap[K]) => void, +) { + const callbackRef = React.useRef(handler); + + if (callbackRef.current !== handler) { + callbackRef.current = handler; + } + + React.useEffect(() => { + const callback = callbackRef.current; + + if (!channel || !callback) { + return; } - return () => { - if (channelRef.current?.onmessage) { - channelRef.current.onmessage = null; - } - channelRef.current?.close(); - }; - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [channelName, initialValue]); + channel.addEventListener(event, callback); - const send = (data: EBCEvent) => { - channelRef.current?.postMessage(data); - }; - - return { mode, value, send }; -}; + return () => channel.removeEventListener(event, callback); + }, [channel, event]); +} diff --git a/widget/src/providers/ChatProvider.tsx b/widget/src/providers/ChatProvider.tsx index 61342785..0bd20df8 100644 --- a/widget/src/providers/ChatProvider.tsx +++ b/widget/src/providers/ChatProvider.tsx @@ -16,11 +16,7 @@ import React, { useState, } from "react"; -import { - EBCEvent, - ETabMode, - useBroadcastChannel, -} from "../hooks/useBroadcastChannel"; +import { useBroadcastChannel } from "../hooks/useBroadcastChannel"; import { StdEventType } from "../types/chat-io-messages.types"; import { Direction, @@ -192,7 +188,6 @@ const ChatProvider: React.FC<{ defaultConnectionState?: ConnectionState; children: ReactNode; }> = ({ wantToConnect, defaultConnectionState = 0, children }) => { - const { mode, value } = useBroadcastChannel(); const config = useConfig(); const settings = useSettings(); const { screen, setScreen } = useWidget(); @@ -424,12 +419,6 @@ const ChatProvider: React.FC<{ // eslint-disable-next-line react-hooks/exhaustive-deps }, [settings.avatarUrl]); - useEffect(() => { - if (value === EBCEvent.LOGOUT_END_SESSION && mode === ETabMode.SECONDARY) { - socketCtx.socket.disconnect(); - } - }, [value, mode, socketCtx.socket]); - const contextValue: ChatContextType = { participants, setParticipants, @@ -464,6 +453,10 @@ const ChatProvider: React.FC<{ handleSubscription, }; + useBroadcastChannel("session", () => { + socketCtx.socket.disconnect(); + }); + return ( {children} );