mango-v4-ui/components/notifications/NotificationsDrawer.tsx

289 lines
9.6 KiB
TypeScript

import Button, { IconButton, LinkButton } from '@components/shared/Button'
import { Dialog, Transition } from '@headlessui/react'
import {
CalendarIcon,
FaceSmileIcon,
InboxIcon,
TrashIcon,
XMarkIcon,
} from '@heroicons/react/20/solid'
import { bs58 } from '@project-serum/anchor/dist/cjs/utils/bytes'
import { WalletContextState, useWallet } from '@solana/wallet-adapter-react'
import { Payload, SIWS } from '@web3auth/sign-in-with-solana'
import { useHeaders } from 'hooks/notifications/useHeaders'
import { useIsAuthorized } from 'hooks/notifications/useIsAuthorized'
import { useNotifications } from 'hooks/notifications/useNotifications'
import { Fragment, useCallback, useEffect, useMemo, useState } from 'react'
import { NOTIFICATION_API } from 'utils/constants'
import NotificationCookieStore from '@store/notificationCookieStore'
import dayjs from 'dayjs'
import { useTranslation } from 'next-i18next'
import { notify } from 'utils/notifications'
export const createSolanaMessage = (
wallet: WalletContextState,
setCookie: (wallet: string, token: string) => void,
) => {
const payload = new Payload()
payload.domain = window.location.host
payload.address = wallet.publicKey!.toString()
payload.uri = window.location.origin
payload.statement = 'Login to Mango Notifications Admin App'
payload.version = '1'
payload.chainId = 1
const message = new SIWS({ payload })
const messageText = message.prepareMessage()
const messageEncoded = new TextEncoder().encode(messageText)
wallet.signMessage!(messageEncoded)
.then(async (resp) => {
const tokenResp = await fetch(`${NOTIFICATION_API}auth`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
...payload,
signatureString: bs58.encode(resp),
}),
})
const body = await tokenResp.json()
const token = body.token
const error = body.error
if (error) {
notify({
type: 'error',
title: 'Error',
description: error,
})
return
}
setCookie(payload.address, token)
})
.catch((e) => {
notify({
type: 'error',
title: 'Error',
description: e.message ? e.message : `${e}`,
})
})
}
const NotificationsDrawer = ({
isOpen,
onClose,
}: {
isOpen: boolean
onClose: () => void
}) => {
const { t } = useTranslation('notifications')
const { data, refetch } = useNotifications()
const wallet = useWallet()
const isAuth = useIsAuthorized()
const headers = useHeaders()
const setCookie = NotificationCookieStore((s) => s.setCookie)
const [isRemoving, setIsRemoving] = useState(false)
const unseenNotifications = useMemo(() => {
if (!data || !data.length) return []
return data.filter((x) => !x.seen)
}, [data])
const markAsSeen = useCallback(
async (ids: number[]) => {
try {
const resp = await fetch(`${NOTIFICATION_API}notifications/seen`, {
method: 'POST',
headers: headers.headers,
body: JSON.stringify({
ids: ids,
seen: true,
}),
})
const body = await resp.json()
const error = body.error
if (error) {
notify({
type: 'error',
title: 'Error',
description: error,
})
return
}
refetch()
} catch (e) {
notify({
type: 'error',
title: 'Error',
description: JSON.stringify(e),
})
}
},
[NOTIFICATION_API, headers],
)
const remove = useCallback(
async (ids: number[]) => {
setIsRemoving(true)
try {
const resp = await fetch(
`${NOTIFICATION_API}notifications/removeForUser`,
{
method: 'POST',
headers: headers.headers,
body: JSON.stringify({
ids: ids,
}),
},
)
const body = await resp.json()
const error = body.error
if (error) {
notify({
type: 'error',
title: 'Error',
description: error,
})
return
}
refetch()
} catch (e) {
notify({
type: 'error',
title: 'Error',
description: JSON.stringify(e),
})
}
setIsRemoving(false)
},
[NOTIFICATION_API, headers],
)
// Mark all notifications as seen when the inbox is opened
useEffect(() => {
if (isOpen && unseenNotifications?.length) {
markAsSeen([...unseenNotifications.map((x) => x.id)])
}
}, [isOpen, unseenNotifications])
return (
<Transition show={isOpen}>
<Dialog className="fixed inset-0 left-0 z-30" onClose={onClose}>
<Transition.Child
enter="transition-opacity ease-linear duration-300"
enterFrom="opacity-0"
enterTo="opacity-100"
leave="transition-opacity ease-linear duration-300"
leaveFrom="opacity-100"
leaveTo="opacity-0"
as={Fragment}
>
<div className="fixed inset-0 z-40 cursor-default bg-black bg-opacity-30" />
</Transition.Child>
<Transition.Child
enter="transition ease-in duration-300"
enterFrom="translate-x-full"
enterTo="translate-x-0"
leave="transition ease-out duration-300"
leaveFrom="translate-x-0"
leaveTo="translate-x-full"
as={Fragment}
>
<Dialog.Panel
className={`thin-scroll absolute right-0 z-40 h-full w-full overflow-y-auto bg-th-bkg-1 text-left md:w-96`}
>
<div className="flex h-16 items-center justify-between border-b border-th-bkg-3 pl-6">
<h2 className="text-lg">{t('notifications')}</h2>
<div className="flex items-center">
{data?.length ? (
<LinkButton
disabled={isRemoving}
className="mr-4 flex items-center text-xs"
onClick={() => remove(data.map((n) => n.id))}
>
<TrashIcon className="mr-1 h-3 w-3" />
<span>{t('clear-all')}</span>
</LinkButton>
) : null}
<button
onClick={onClose}
className="flex h-16 w-16 items-center justify-center border-l border-th-bkg-3 text-th-fgd-3 focus-visible:bg-th-bkg-3 md:hover:bg-th-bkg-2"
>
<XMarkIcon className={`h-5 w-5`} />
</button>
</div>
</div>
{isAuth ? (
<>
{data?.length ? (
<>
<div className="space-y-4 border-b border-th-bkg-3 pb-4">
{data?.map((notification) => (
<div
className="border-t border-th-bkg-3 pt-4 first:border-t-0"
key={notification.id}
>
<div className="px-6">
<div className="mb-1 flex items-start justify-between">
<h4 className="mr-4">{notification.title}</h4>
<IconButton
disabled={isRemoving}
onClick={() => remove([notification.id])}
className="mt-1 text-th-fgd-3"
hideBg
>
<TrashIcon className="h-4 w-4" />
</IconButton>
</div>
<div className="mb-2 flex items-center text-th-fgd-4">
<CalendarIcon className="mr-1 h-3.5 w-3.5" />
<p className="text-xs">
{dayjs(notification.createdAt).format(
'DD MMM YYYY, h:mma',
)}
</p>
</div>
<p>{notification.content}</p>
</div>
</div>
))}
</div>
</>
) : (
<div className="relative top-1/2 flex -translate-y-1/2 flex-col justify-center px-6 pb-20">
<div className="flex flex-col items-center justify-center text-center">
<FaceSmileIcon className="mb-2 h-7 w-7 text-th-fgd-2" />
<h3 className="mb-1 text-base">
{t('empty-state-title')}
</h3>
<p>{t('empty-state-desc')}</p>
</div>
</div>
)}
</>
) : (
<div className="relative top-1/2 flex -translate-y-1/2 flex-col justify-center px-6 pb-20">
<div className="flex flex-col items-center justify-center text-center">
<InboxIcon className="mb-2 h-7 w-7 text-th-fgd-2" />
<h3 className="mb-1 text-base">{t('unauth-title')}</h3>
<p>{t('unauth-desc')}</p>
<Button
className="mt-6"
onClick={() => createSolanaMessage(wallet, setCookie)}
>
{t('sign-message')}
</Button>
</div>
</div>
)}
</Dialog.Panel>
</Transition.Child>
</Dialog>
</Transition>
)
}
export default NotificationsDrawer