/** * @file useChatOverlay.ts * @description Hook for managing the global chat overlay state */ import { useState, useEffect, useCallback, useRef } from "react"; import { safeJsonParse, isChatOverlayState } from "@/lib/utils/safe-json"; interface ChatOverlayState { isOpen: boolean; isMinimized: boolean; } export interface UseChatOverlayOptions { onStorageError?: (message: string) => void; } interface UseChatOverlayResult extends ChatOverlayState { open: () => void; close: () => void; minimize: () => void; expand: () => void; toggle: () => void; toggleMinimize: () => void; } const STORAGE_KEY = "chatOverlayState"; const DEFAULT_STATE: ChatOverlayState = { isOpen: false, isMinimized: false, }; /** * Get a user-friendly error message for localStorage failures */ function getStorageErrorMessage(error: unknown): string { if (error instanceof DOMException) { switch (error.name) { case "QuotaExceededError": return "Storage is full. Chat state may not persist across page refreshes."; case "SecurityError": return "Storage is unavailable in this browser mode. Chat state will not persist."; case "InvalidStateError": return "Storage is disabled. Chat state will not persist across page refreshes."; } } return "Unable to save chat state. It may not persist across page refreshes."; } /** * Load state from localStorage with runtime type validation */ function loadState(): ChatOverlayState { if (typeof window === "undefined") { return DEFAULT_STATE; } try { const stored = window.localStorage.getItem(STORAGE_KEY); if (stored) { return safeJsonParse(stored, isChatOverlayState, DEFAULT_STATE); } } catch (error) { console.warn("Failed to load chat overlay state from localStorage:", error); } return DEFAULT_STATE; } /** * Save state to localStorage, notifying on error (once per error type) */ function saveState( state: ChatOverlayState, onStorageError: ((message: string) => void) | undefined, notifiedErrors: Set ): void { if (typeof window === "undefined") { return; } try { window.localStorage.setItem(STORAGE_KEY, JSON.stringify(state)); } catch (error) { const errorName = error instanceof DOMException ? error.name : error instanceof Error ? error.constructor.name : "UnknownError"; console.warn("Failed to save chat overlay state to localStorage:", error); if (onStorageError && !notifiedErrors.has(errorName)) { notifiedErrors.add(errorName); onStorageError(getStorageErrorMessage(error)); } } } /** * Custom hook for managing chat overlay state * Persists state to localStorage for consistency across page refreshes. * Enforces invariant: cannot be minimized when closed. */ export function useChatOverlay(options: UseChatOverlayOptions = {}): UseChatOverlayResult { const { onStorageError } = options; const [state, setState] = useState(loadState); const notifiedErrorsRef = useRef>(new Set()); const onStorageErrorRef = useRef(onStorageError); onStorageErrorRef.current = onStorageError; // Persist state changes to localStorage useEffect(() => { saveState(state, onStorageErrorRef.current, notifiedErrorsRef.current); }, [state]); const open = useCallback(() => { setState({ isOpen: true, isMinimized: false }); }, []); const close = useCallback(() => { setState({ isOpen: false, isMinimized: false }); }, []); const minimize = useCallback(() => { setState((prev) => (prev.isOpen ? { ...prev, isMinimized: true } : prev)); }, []); const expand = useCallback(() => { setState((prev) => ({ ...prev, isMinimized: false })); }, []); const toggle = useCallback(() => { setState((prev) => { const newIsOpen = !prev.isOpen; return { isOpen: newIsOpen, isMinimized: newIsOpen ? prev.isMinimized : false }; }); }, []); const toggleMinimize = useCallback(() => { setState((prev) => (prev.isOpen ? { ...prev, isMinimized: !prev.isMinimized } : prev)); }, []); return { ...state, open, close, minimize, expand, toggle, toggleMinimize, }; }