import type { VisibilityOptions } from 'config.types' import { useConfig } from 'domains/config/hooks' import { useAppDispatch } from 'domains/store' import { setVisibility } from 'domains/visibility/actions' import { setShowInlineView } from 'domains/visibility/slice' import type { VisibilityActionArgs } from 'domains/visibility/visibility.types' import { RefObject, createRef } from 'preact' import { useCallback, useEffect, useState } from 'preact/hooks' import { useSelector } from 'react-redux' import { visibilityStates } from './constants' import { selectShowInlineView, selectVisibility, selectSetInputFocus, selectShowPreChat, selectShowContinueChat, } from './selectors' export const useVisibility = () => { const dispatch = useAppDispatch() const visible = useSelector(selectVisibility) const setInputFocus = useSelector(selectSetInputFocus) const showPreChat = useSelector(selectShowPreChat) const showContinueChat = useSelector(selectShowContinueChat) const isVisible = visible ? visible !== visibilityStates.hidden : false const isOpen = visible === visibilityStates.open const isMinimized = visible === visibilityStates.minimized const dispatchVisibility = useCallback( (args: VisibilityActionArgs | VisibilityOptions) => { if (typeof args === 'string' || args === null) { dispatch(setVisibility({ visibility: args, setInputFocus: false })) } else { dispatch(setVisibility(args)) } }, [dispatch], ) const openChat = () => { dispatchVisibility({ visibility: visibilityStates.open }) } const closeChat = () => { dispatchVisibility({ visibility: visibilityStates.minimized }) } const closePreChat = () => { dispatchVisibility({ visibility: visible, showPreChat: false }) } const closeContinueChat = () => { dispatchVisibility({ visibility: visible, showContinueChat: false }) } return { isVisible, isOpen, isMinimized, visible, setVisibility: dispatchVisibility, openChat, closeChat, setInputFocus, showPreChat, closePreChat, showContinueChat, closeContinueChat, } } type UseIntersectOptions = { /** Stops observing when the root element is visible. */ freezeOnceVisible?: boolean /** Determines if useIntersect is enabled. */ enabled?: boolean /** The node ref to apply the intersection to */ containerRef?: RefObject } /** * Custom hook which enables initializing of IntersectionObserver on any node ref. */ export const useIntersect = ({ freezeOnceVisible = false, enabled = true, containerRef = createRef(), }: UseIntersectOptions) => { const [entry, setEntry] = useState(null) const isVisible = !!entry?.isIntersecting || !enabled const frozen = isVisible && freezeOnceVisible const observerCallback: { ( _entries: IntersectionObserverEntry[], _observer: IntersectionObserver, ): void } = ([updatedEntry]) => setEntry(updatedEntry) useEffect(() => { const node = containerRef?.current const hasIOSupport = !!window.IntersectionObserver if (!node && process.env.NODE_ENV === 'development') { console.error( 'containerRef must be set on a DOM element. check the component where useIntersect is being used.', ) } // Return an arrow function to have a consistent return value if (!hasIOSupport || frozen || !node || !enabled) return () => undefined const observerOptions = { threshold: 0, root: null, rootMargin: '0%' } const observer = new IntersectionObserver(observerCallback, observerOptions) observer.observe(node) return () => observer.disconnect() }, [containerRef, enabled, frozen]) return { isVisible, containerRef } } export const useShowInlineView = () => { const dispatch = useAppDispatch() const { connectWhenInView } = useConfig() const showInlineView = useSelector(selectShowInlineView) const { containerRef, isVisible } = useIntersect({ enabled: connectWhenInView, freezeOnceVisible: true, }) useEffect(() => { if (!isVisible) return dispatch(setShowInlineView()) }, [dispatch, isVisible]) return { containerRef, showInlineView } }