import { useUserHasResponded } from 'domains/app/hooks' import { useConfig } from 'domains/config/hooks' import { useI18n } from 'domains/i18n/hooks' import { selectHasError } from 'domains/interrupt/selectors' import { ServiceDataEvent } from 'domains/store/store.types' import { useTranslatedEventData } from 'domains/translations/hooks' import { visibilityStates } from 'domains/visibility/constants' import { useVisibility } from 'domains/visibility/hooks' import { className } from 'lib/css' import { randomId } from 'lib/id' import { useCallback, useEffect, useMemo, useRef } from 'preact/hooks' import { useSelector } from 'react-redux' import SuggestionsList from 'ui/components/suggestions/suggestions-list' import InOutTransition, { transitionStartStates, } from 'ui/components/widgets/in-out-transition' import { useSkiplinkTargetFocusing } from 'ui/hooks/focus-helper-hooks' import { useLiveRegion } from 'ui/hooks/live-region-hooks' import { useSeamlyHasConversation } from 'ui/hooks/seamly-api-hooks' import { useSeamlyLayoutMode, useSeamlyServiceData, } from 'ui/hooks/seamly-state-hooks' import useSeamlyCommands from 'ui/hooks/use-seamly-commands' import useSeamlyIdleDetachCountdown from 'ui/hooks/use-seamly-idle-detach-countdown' import useSeamlyResumeConversationPrompt from 'ui/hooks/use-seamly-resume-conversation-prompt' import { useGeneratedId } from 'ui/hooks/utility-hooks' import { runIfElementContainsOrHasFocus } from 'ui/utils/general-utils' import { actionTypes } from 'ui/utils/seamly-utils' const Suggestions = ({ isAside = false }) => { // generic hooks const { isInline } = useSeamlyLayoutMode() const { t } = useI18n() const { addMessageBubble, emitEvent, sendAction } = useSeamlyCommands() const hasConversation = useSeamlyHasConversation() const { isOpen, setVisibility } = useVisibility() const { showSuggestions } = useConfig() // a11y hooks const sectionId = useGeneratedId() const focusSkiplinkTarget = useSkiplinkTargetFocusing() const containerRef = useRef(null) const { sendPolite } = useLiveRegion() // interrupt & countdown hooks const hasError = useSelector(selectHasError) const { hasCountdown, endCountdown } = useSeamlyIdleDetachCountdown() const { hasPrompt, continueChat } = useSeamlyResumeConversationPrompt() // data hooks const userHasResponded = useUserHasResponded() const payload = useSeamlyServiceData('suggestion') const { body: eventBody } = useTranslatedEventData({ payload, type: 'service_data', }) const suggestions = useMemo(() => { if (hasError) return [] if (Array.isArray(eventBody)) { return eventBody } return [] }, [eventBody, hasError]) const prevSuggestions = useRef() const prevHasSuggestions = useRef(false) const previousRenderedSuggestions = useRef() const hasSuggestions = !!suggestions?.length const hideSuggestions = isInline ? (userHasResponded || isOpen) && !isAside : userHasResponded const prevHideSuggestions = useRef(hideSuggestions) const showSuggestionsContainer = hasSuggestions && !hideSuggestions && showSuggestions const renderedSuggestions = hasSuggestions ? suggestions : previousRenderedSuggestions.current previousRenderedSuggestions.current = renderedSuggestions const suggestionsClassNames = useMemo(() => { const classNames = ['suggestions'] if (isAside) { classNames.push('suggestions--aside') } return className(classNames) }, [isAside]) // click handler const handleClick = useCallback( ({ id, question, }: { id: string | number | undefined question?: string | undefined }) => { // Do not allow interaction without a conversation if (!hasConversation()) { return } if (hasCountdown) { endCountdown(true) } if (hasPrompt) { continueChat() } const transactionId = randomId() const action = { type: actionTypes.custom, originMessage: payload.id, body: { type: 'faqclick', body: { faqId: id, faqQuestion: question, }, }, transactionId, } // @todo Refactor to 'suggestionclick' sendAction(action) addMessageBubble(question, transactionId) emitEvent(`action.${action.type}`, action) if (!isOpen) { setVisibility({ visibility: visibilityStates.open }) } focusSkiplinkTarget() }, [ addMessageBubble, continueChat, endCountdown, emitEvent, focusSkiplinkTarget, hasConversation, hasCountdown, hasPrompt, isOpen, payload, sendAction, setVisibility, ], ) useEffect(() => { if (prevSuggestions.current !== suggestions && !hideSuggestions) { if (hasSuggestions) { const politeText = prevHasSuggestions.current ? t('suggestions.srUpdatedText') : t('suggestions.srAvailableText') setTimeout(() => { sendPolite(politeText) }, 30) } else if (prevHasSuggestions.current) { sendPolite(t('suggestions.srUnavailableText')) } prevSuggestions.current = suggestions } if (!prevHideSuggestions.current && hideSuggestions) { runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget) sendPolite(t('suggestions.srUnavailableText')) } else if (!hasSuggestions && prevHasSuggestions.current) { runIfElementContainsOrHasFocus(containerRef.current, focusSkiplinkTarget) } prevHasSuggestions.current = hasSuggestions prevHideSuggestions.current = hideSuggestions }, [ suggestions, hasSuggestions, hideSuggestions, focusSkiplinkTarget, sendPolite, t, ]) const headingText = t('suggestions.headingText') const footerText = t('suggestions.footerText') const ContainerElement = headingText ? 'section' : 'div' return ( {headingText && (

{headingText}

)} {!!renderedSuggestions && ( )} {footerText && !isOpen && (

{footerText}

)}
) } export default Suggestions