mango-v4-ui/components/shared/Notification.tsx

282 lines
8.6 KiB
TypeScript
Raw Normal View History

2022-10-28 03:42:23 -07:00
import { Fragment, useEffect, useMemo, useState } from 'react'
2022-07-05 20:37:49 -07:00
import {
CheckCircleIcon,
2022-09-06 21:36:35 -07:00
ArrowTopRightOnSquareIcon,
2022-07-05 20:37:49 -07:00
InformationCircleIcon,
XCircleIcon,
2022-09-06 21:36:35 -07:00
} from '@heroicons/react/20/solid'
2022-09-12 08:53:57 -07:00
import mangoStore, { CLUSTER } from '@store/mangoStore'
2022-07-05 20:37:49 -07:00
import { Notification, notify } from '../../utils/notifications'
import Loading from './Loading'
import { Transition } from '@headlessui/react'
import { TokenInstructions } from '@project-serum/serum'
2022-10-09 20:07:35 -07:00
import {
CLIENT_TX_TIMEOUT,
NOTIFICATION_POSITION_KEY,
2022-10-31 09:39:33 -07:00
PREFERRED_EXPLORER_KEY,
2022-10-09 20:07:35 -07:00
} from '../../utils/constants'
import useLocalStorageState from 'hooks/useLocalStorageState'
2022-10-31 09:39:33 -07:00
import { EXPLORERS } from 'pages/settings'
2022-07-05 20:37:49 -07:00
const setMangoStore = mangoStore.getState().set
2022-07-05 20:37:49 -07:00
const NotificationList = () => {
2022-07-22 08:40:53 -07:00
const notifications = mangoStore((s) => s.notifications)
const walletTokens = mangoStore((s) => s.wallet.tokens)
2022-07-05 20:37:49 -07:00
const notEnoughSoLMessage = 'Not enough SOL'
2022-10-09 20:07:35 -07:00
const [notificationPosition] = useLocalStorageState(
NOTIFICATION_POSITION_KEY,
'bottom-left'
)
2022-10-28 03:42:23 -07:00
const [mounted, setMounted] = useState(false)
2022-07-05 20:37:49 -07:00
// if a notification is shown with {"InstructionError":[0,{"Custom":1}]} then
// add a notification letting the user know they may not have enough SOL
useEffect(() => {
if (notifications.length) {
const customErrorNotification = notifications.find(
(n) => n.description && n.description.includes('"Custom":1')
)
const notEnoughSolNotification = notifications.find(
(n) => n.title && n.title.includes(notEnoughSoLMessage)
)
const solBalance = walletTokens.find((t) =>
t.mint.equals(TokenInstructions.WRAPPED_SOL_MINT)
)?.uiAmount
if (
!notEnoughSolNotification &&
customErrorNotification &&
solBalance &&
solBalance < 0.04
) {
notify({
title: notEnoughSoLMessage,
type: 'info',
})
}
}
}, [notifications, walletTokens])
const reversedNotifications = [...notifications].reverse()
2022-10-28 03:42:23 -07:00
const position: string = useMemo(() => {
2022-10-09 20:07:35 -07:00
switch (notificationPosition) {
case 'bottom-left':
2022-10-28 03:42:23 -07:00
return 'bottom-0 left-0'
2022-10-09 20:07:35 -07:00
case 'bottom-right':
2022-10-28 03:42:23 -07:00
return 'bottom-0 right-0'
2022-10-09 20:07:35 -07:00
case 'top-left':
2022-10-28 03:42:23 -07:00
return 'top-0 left-0'
2022-10-09 20:07:35 -07:00
case 'top-right':
2022-10-28 03:42:23 -07:00
return 'top-0 right-0'
2022-10-09 20:07:35 -07:00
default:
2022-10-28 03:42:23 -07:00
return 'bottom-0 left-0'
2022-10-09 20:07:35 -07:00
}
}, [notificationPosition])
2022-10-28 03:42:23 -07:00
useEffect(() => setMounted(true), [])
if (!mounted) return null
2022-07-05 20:37:49 -07:00
return (
<div
2022-10-28 03:42:23 -07:00
className={`pointer-events-none fixed z-50 w-full space-y-2 p-4 text-th-fgd-1 md:w-auto md:p-6 ${position}`}
2022-07-05 20:37:49 -07:00
>
2022-10-09 20:07:35 -07:00
{reversedNotifications.map((n) => (
<Notification key={n.id} notification={n} />
))}
2022-07-05 20:37:49 -07:00
</div>
)
}
const Notification = ({ notification }: { notification: Notification }) => {
2022-10-09 20:07:35 -07:00
const [notificationPosition] = useLocalStorageState(
NOTIFICATION_POSITION_KEY,
'bottom-left'
)
2022-10-31 09:39:33 -07:00
const [preferredExplorer] = useLocalStorageState(
PREFERRED_EXPLORER_KEY,
EXPLORERS[0]
)
2022-07-05 20:37:49 -07:00
const { type, title, description, txid, show, id } = notification
// overwrite the title if of the error message if it is a time out error
let parsedTitle: string | undefined
if (description) {
if (
description?.includes('Timed out awaiting') ||
description?.includes('was not confirmed')
) {
parsedTitle = 'Transaction status unknown'
}
}
// if the notification is a success, then hide the confirming tx notification with the same txid
useEffect(() => {
if ((type === 'error' || type === 'success') && txid) {
setMangoStore((s) => {
const newNotifications = s.notifications.map((n) =>
n.txid === txid && n.type === 'confirm' ? { ...n, show: false } : n
)
s.notifications = newNotifications
})
}
}, [type, txid])
const hideNotification = () => {
setMangoStore((s) => {
const newNotifications = s.notifications.map((n) =>
n.id === id ? { ...n, show: false } : n
)
s.notifications = newNotifications
})
}
// auto hide a notification after 8 seconds unless it is a confirming or time out notification
// if no status is provided for a tx notification after 90s, hide it
useEffect(() => {
const id = setTimeout(
() => {
if (show) {
hideNotification()
}
},
2022-09-29 21:21:23 -07:00
parsedTitle || type === 'confirm'
2022-07-05 20:37:49 -07:00
? CLIENT_TX_TIMEOUT
2022-09-29 21:21:23 -07:00
: type === 'error'
? 30000
2022-07-05 20:37:49 -07:00
: 8000
)
return () => {
clearInterval(id)
}
})
2022-10-09 20:07:35 -07:00
const {
enterFromClass,
enterToClass,
leaveFromClass,
leaveToClass,
}: {
enterFromClass: string
enterToClass: string
leaveFromClass: string
leaveToClass: string
} = useMemo(() => {
const fromLeft = {
enterFromClass: 'md:-translate-x-48',
enterToClass: 'md:translate-x-100',
leaveFromClass: 'md:translate-x-0',
leaveToClass: 'md:-translate-x-48',
}
const fromRight = {
enterFromClass: 'md:translate-x-48',
enterToClass: 'md:-translate-x-100',
leaveFromClass: 'md:translate-x-0',
leaveToClass: 'md:translate-x-48',
}
switch (notificationPosition) {
case 'bottom-left':
return fromLeft
case 'bottom-right':
return fromRight
case 'top-left':
return fromLeft
case 'top-right':
return fromRight
default:
return fromLeft
}
}, [notificationPosition])
2022-07-05 20:37:49 -07:00
return (
<Transition
show={show}
as={Fragment}
appear={true}
enter="ease-out duration-500 transition"
2022-10-09 20:07:35 -07:00
enterFrom={`-translate-y-2 opacity-0 md:translate-y-0 ${enterFromClass}`}
enterTo={`translate-y-0 opacity-100 ${enterToClass}`}
leave="ease-in duration-200 transition"
2022-10-09 20:07:35 -07:00
leaveFrom={`translate-y-0 ${leaveFromClass}`}
leaveTo={`-translate-y-2 md:translate-y-0 ${leaveToClass}`}
2022-07-05 20:37:49 -07:00
>
<div
2022-10-09 20:07:35 -07:00
className={`pointer-events-auto w-full rounded-md border bg-th-bkg-2 shadow-lg md:w-auto ${
2022-10-08 03:15:03 -07:00
type === 'success'
? 'border-th-green'
: type === 'error'
? 'border-th-red'
: 'border-th-bkg-4'
}`}
2022-07-05 20:37:49 -07:00
>
2022-10-09 20:07:35 -07:00
<div className={`relative flex w-full items-center p-3.5 md:w-96`}>
2022-10-08 03:15:03 -07:00
<div className={`mr-1 flex-shrink-0`}>
2022-07-05 20:37:49 -07:00
{type === 'success' ? (
2022-10-08 03:15:03 -07:00
<CheckCircleIcon className={`h-6 w-6 text-th-green`} />
2022-07-05 20:37:49 -07:00
) : null}
{type === 'info' && (
2022-10-08 03:15:03 -07:00
<InformationCircleIcon className={`h-6 w-6 text-th-fgd-3`} />
2022-07-05 20:37:49 -07:00
)}
{type === 'error' && (
2022-10-08 03:15:03 -07:00
<XCircleIcon className={`h-6 w-6 text-th-red`} />
2022-07-05 20:37:49 -07:00
)}
{type === 'confirm' && (
2022-10-08 03:15:03 -07:00
<Loading className="mr-0.5 h-5 w-5 text-th-primary" />
2022-07-05 20:37:49 -07:00
)}
</div>
<div className={`ml-2 flex-1`}>
2022-10-08 03:15:03 -07:00
<p className={`text-th-fgd-1`}>{parsedTitle || title}</p>
2022-07-05 20:37:49 -07:00
{description ? (
2022-09-29 21:21:23 -07:00
<p
2022-10-08 03:15:03 -07:00
className={`mb-0 mt-0.5 break-all text-sm leading-tight text-th-fgd-4`}
2022-09-29 21:21:23 -07:00
>
2022-07-05 20:37:49 -07:00
{description}
</p>
) : null}
{txid ? (
<a
2022-10-31 09:39:33 -07:00
href={preferredExplorer.url + txid + '?cluster=' + CLUSTER}
2022-10-08 03:15:03 -07:00
className="default-transition mt-1 flex items-center text-xs text-th-fgd-3 hover:text-th-fgd-2"
2022-07-05 20:37:49 -07:00
target="_blank"
rel="noreferrer"
>
2022-10-08 03:15:03 -07:00
<div className="break-all">
2022-07-05 20:37:49 -07:00
{type === 'error'
? txid
: `${txid.slice(0, 14)}...${txid.slice(txid.length - 14)}`}
</div>
2022-09-06 21:36:35 -07:00
<ArrowTopRightOnSquareIcon className="mb-0.5 ml-1 h-4 w-4" />
2022-07-05 20:37:49 -07:00
</a>
) : null}
</div>
<div className={`absolute right-2 top-2 flex-shrink-0`}>
<button
onClick={hideNotification}
2022-10-08 03:15:03 -07:00
className={`default-transition text-th-fgd-4 focus:outline-none md:hover:text-th-fgd-3`}
2022-07-05 20:37:49 -07:00
>
<span className={`sr-only`}>Close</span>
<svg
className={`h-5 w-5`}
xmlns="http://www.w3.org/2000/svg"
viewBox="0 0 20 20"
fill="currentColor"
aria-hidden="true"
>
<path
fillRule="evenodd"
d="M4.293 4.293a1 1 0 011.414 0L10 8.586l4.293-4.293a1 1 0 111.414 1.414L11.414 10l4.293 4.293a1 1 0 01-1.414 1.414L10 11.414l-4.293 4.293a1 1 0 01-1.414-1.414L8.586 10 4.293 5.707a1 1 0 010-1.414z"
clipRule="evenodd"
/>
</svg>
</button>
</div>
</div>
</div>
</Transition>
)
}
export default NotificationList