import React, { cloneElement, FC, isValidElement, useEffect, useRef, useState } from 'react' import { AsyncMethodReturns, connectToChild } from 'penpal' import { Popover, PopoverTrigger, PopoverContent, Box, useDisclosure, useOutsideClick, chakra, Button, Portal, } from '@chakra-ui/react' import maxSize from 'popper-max-size-modifier' import { NotificationsMenuTrigger, NotificationsTriggerTheme } from './notifications-trigger' import { HNS_HC_NOTIFICATIONS_LIST_URL } from '../constants' import { ApolloError } from '@apollo/client' import { apolloClient } from '../apollo-client' import { useUnreadCountQueryQuery } from '../graphql' import { useHNSContext } from '../provider' const GUTTER = 16 const applyMaxSize = { name: 'applyMaxSize', enabled: true, phase: 'beforeWrite', requires: ['maxSize'], fn({ state }: any) { // The `maxSize` modifier provides this data const { height } = state.modifiersData.maxSize state.styles.popper = { ...state.styles.popper, height: `${height - GUTTER}px`, display: 'flex', flexDirection: 'column', } }, } export interface NotificationsPopoverProps { buttonTheme?: NotificationsTriggerTheme unreadCount?: number onNotificationClick?: (id: string, clickUrl?: string) => void closeOnNotificationClick?: boolean buttonComponent?: React.ReactNode } interface ChildApi { setIsNotificationCenterVisible: (value: boolean) => void setIsWebPushSubscribed: (value: boolean) => void } export const NotificationsPopover: FC = ({ unreadCount, buttonTheme, onNotificationClick: onNotificationClickProp, closeOnNotificationClick = true, buttonComponent = , }) => { const { getUserToken, subscribeToWebPush, isWebPushSubscribed } = useHNSContext() const [mounted, setMounted] = useState(false) // we need this ref because inside the useEffect the state is not updated yet const refisOpened = useRef(false) const iframeRef = useRef(null) const containerRef = useRef(null) const connection = useRef>() const { isOpen, onOpen, onClose } = useDisclosure({ onClose() { refisOpened.current = false connection.current?.setIsNotificationCenterVisible?.(false) }, onOpen() { console.log('onOpen', connection) refisOpened.current = true connection.current?.setIsNotificationCenterVisible?.(true) }, }) useEffect(() => { if (!mounted || !iframeRef.current || !isOpen) { return } const newConn = connectToChild({ // The iframe to which a connection should be made iframe: iframeRef.current, // Methods the parent is exposing to the child methods: { getUserToken, refetchUnreadCount: refetch, subscribeToWebPush, isWebPushSubscribed: async () => { // navigator.serviceWorker.getRegistration("/hns-webpush-sw.js") return isWebPushSubscribed }, onNotificationClick: (id: string, clickUrl?: string) => { if (onNotificationClickProp) { onNotificationClickProp(id, clickUrl) } if (closeOnNotificationClick) { onClose() } }, }, }) newConn.promise.then((child) => { connection.current = child if (child.setIsNotificationCenterVisible) { child.setIsNotificationCenterVisible?.(true) } child.setIsWebPushSubscribed?.(isWebPushSubscribed) }) return () => { if (newConn) { newConn.promise.then((child) => { if (child.setIsNotificationCenterVisible) { child.setIsNotificationCenterVisible?.(false) } }) } } // eslint-disable-next-line react-hooks/exhaustive-deps }, [getUserToken, isOpen]) useEffect(() => { if (connection.current) { console.log('setIsWebPushSubscribed', connection, isWebPushSubscribed) connection.current?.setIsWebPushSubscribed(isWebPushSubscribed) } }, [isWebPushSubscribed]) const [jwt, setJwt] = React.useState() const { loading, data, refetch } = useUnreadCountQueryQuery({ client: apolloClient, skip: !jwt, ssr: false, context: { headers: { authorization: `Bearer ${jwt}`, }, }, notifyOnNetworkStatusChange: true, }) useOutsideClick({ ref: containerRef, enabled: isOpen, handler: (e) => { if (e.target instanceof HTMLElement && e.target.closest('[data-action="notifications-menu"]')) { return } else { onClose() } }, }) useEffect(() => { let ignore = false const getToken = async () => { const token = await getUserToken() if (!ignore) { setJwt(token) } } getToken() return () => { ignore = true } }, [getUserToken]) useEffect(() => { setMounted(true) return () => { setMounted(false) } }, []) useEffect(() => { const interval = setInterval(async () => { try { await refetch() } catch (e) { if (e instanceof ApolloError && e.message === 'Not authorized') { const token = await getUserToken() setJwt(token) } } }, 10 * 1000) return () => clearInterval(interval) }, []) if (!mounted) { return ( <> {isValidElement(buttonComponent) && typeof buttonComponent !== 'string' ? ( cloneElement( buttonComponent as any, { isActive: false, isLoading: true, unreadCount: data?.unreadCount || 0, 'data-action': 'notifications-menu', isDisabled: true, } as any ) ) : typeof buttonComponent === 'string' ? ( ) : ( buttonComponent )} ) } return ( {isValidElement(buttonComponent) && typeof buttonComponent !== 'string' ? ( cloneElement( buttonComponent as any, { isActive: isOpen, isLoading: loading, unreadCount: data?.unreadCount || 0, 'data-action': 'notifications-menu', } as any ) ) : typeof buttonComponent === 'string' ? ( ) : ( buttonComponent )} ) }