345 lines
11 KiB
TypeScript
345 lines
11 KiB
TypeScript
import Avatar from '@/components/ui/Avatar'
|
|
import Badge from '@/components/ui/Badge'
|
|
import Button from '@/components/ui/Button'
|
|
import Dropdown from '@/components/ui/Dropdown'
|
|
import ScrollBar from '@/components/ui/ScrollBar'
|
|
import Spinner from '@/components/ui/Spinner'
|
|
import Tooltip from '@/components/ui/Tooltip'
|
|
import { AVATAR_URL } from '@/constants/app.constant'
|
|
import NotificationChannels from '@/constants/notification-channel.enum'
|
|
import { ROUTES_ENUM } from '@/constants/route.constant'
|
|
import {
|
|
getList,
|
|
updateRead,
|
|
updateReadAll,
|
|
updateSent,
|
|
} from '@/proxy/notification/notification.service'
|
|
import { useStoreState } from '@/store'
|
|
import withHeaderItem from '@/utils/hoc/withHeaderItem'
|
|
import { useLocalization } from '@/utils/hooks/useLocalization'
|
|
import useResponsive from '@/utils/hooks/useResponsive'
|
|
import useThemeClass from '@/utils/hooks/useThemeClass'
|
|
import isLastChild from '@/utils/isLastChild'
|
|
import classNames from 'classnames'
|
|
import dayjs from 'dayjs'
|
|
import relativeTime from 'dayjs/plugin/relativeTime'
|
|
import { useCallback, useEffect, useRef, useState } from 'react'
|
|
import { HiOutlineBell, HiOutlineMailOpen } from 'react-icons/hi'
|
|
import { Link } from 'react-router-dom'
|
|
import { Notification as Notify, toast } from '../ui'
|
|
|
|
dayjs.extend(relativeTime)
|
|
|
|
type NotificationList = {
|
|
id: string
|
|
creatorId: string
|
|
tenantId?: string
|
|
notificationType: string
|
|
message: string
|
|
date: string
|
|
readed: boolean
|
|
}
|
|
|
|
const notificationHeight = 'h-72'
|
|
const notificationInterval = 120000
|
|
|
|
const notificationTypeAvatar = (creatorId: string, tenantId?: string) => {
|
|
return <Avatar shape="circle" src={AVATAR_URL(creatorId, tenantId)} />
|
|
}
|
|
|
|
const NotificationToggle = ({
|
|
className,
|
|
unreadCount,
|
|
}: {
|
|
className?: string
|
|
unreadCount: number
|
|
}) => {
|
|
return (
|
|
<div className={classNames('text-2xl', className)}>
|
|
{unreadCount > 0 ? (
|
|
<Badge
|
|
badgeStyle={{ top: '3px', right: '6px' }}
|
|
content={unreadCount}
|
|
innerClass="py-0 px-1"
|
|
>
|
|
<HiOutlineBell />
|
|
</Badge>
|
|
) : (
|
|
<HiOutlineBell />
|
|
)}
|
|
</div>
|
|
)
|
|
}
|
|
|
|
const _Notification = ({ className }: { className?: string }) => {
|
|
const { translate } = useLocalization()
|
|
|
|
const [notificationList, setNotificationList] = useState<NotificationList[]>([])
|
|
const [unreadNotificationCount, setUnreadNotificationCount] = useState(0)
|
|
const [noResult, setNoResult] = useState(false)
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
const [toastNotificationList, setToastNotificationList] = useState<string[]>([])
|
|
const toastNotificationInterval = useRef<NodeJS.Timer>()
|
|
const [desktopNotificationList, setDesktopNotificationList] = useState<string[]>([])
|
|
const desktopNotificationInterval = useRef<NodeJS.Timer>()
|
|
|
|
const { bgTheme } = useThemeClass()
|
|
const { larger } = useResponsive()
|
|
|
|
const direction = useStoreState((state) => state.theme.direction)
|
|
const tabHasFocus = useStoreState((a) => a.base.common.tabHasFocus)
|
|
|
|
const getReactNotificationCount = useCallback(async () => {
|
|
const resp = await getList({
|
|
channels: [NotificationChannels.UiActivity],
|
|
isListRequest: false,
|
|
isRead: false,
|
|
maxResultCount: 1,
|
|
})
|
|
setUnreadNotificationCount(resp.data?.totalCount ?? 0)
|
|
}, [setUnreadNotificationCount])
|
|
|
|
useEffect(() => {
|
|
getReactNotificationCount()
|
|
var intervalId = setInterval(() => {
|
|
getReactNotificationCount()
|
|
}, notificationInterval)
|
|
|
|
return () => {
|
|
clearInterval(intervalId)
|
|
}
|
|
}, [getReactNotificationCount])
|
|
|
|
const onNotificationOpen = useCallback(async () => {
|
|
const currentUnread = notificationList.filter((a) => !a.readed).length
|
|
if (currentUnread !== unreadNotificationCount) {
|
|
setLoading(true)
|
|
const resp = await getList({
|
|
channels: [NotificationChannels.UiActivity],
|
|
isListRequest: false,
|
|
maxResultCount: 1000,
|
|
})
|
|
const items = resp.data.items ?? []
|
|
for (const notification of items) {
|
|
await updateSent(notification.id, true)
|
|
}
|
|
const newNotificationList = items.map(
|
|
(a) =>
|
|
({
|
|
id: a.id,
|
|
notificationType: a.notificationType,
|
|
date: a.creationTime.toLocaleString(),
|
|
message: a.message,
|
|
creatorId: a.creatorId,
|
|
tenantId: a.tenantId,
|
|
readed: a.isRead,
|
|
}) as NotificationList,
|
|
)
|
|
|
|
setLoading(false)
|
|
setNotificationList(newNotificationList)
|
|
setNoResult(newNotificationList.length == 0)
|
|
}
|
|
}, [notificationList, setLoading, unreadNotificationCount])
|
|
|
|
const onMarkAllAsRead = useCallback(async () => {
|
|
await updateReadAll(NotificationChannels.UiToast, true)
|
|
|
|
const list = notificationList.map((item: NotificationList) => {
|
|
if (!item.readed) {
|
|
item.readed = true
|
|
}
|
|
return item
|
|
})
|
|
setNotificationList(list)
|
|
setUnreadNotificationCount(0)
|
|
}, [notificationList])
|
|
|
|
const onMarkAsRead = useCallback(
|
|
async (id: string) => {
|
|
await updateRead(id, true)
|
|
|
|
const list = notificationList.map((item) => {
|
|
if (item.id === id) {
|
|
item.readed = true
|
|
}
|
|
return item
|
|
})
|
|
setNotificationList(list)
|
|
|
|
const unreadCount = notificationList.filter((item) => !item.readed).length
|
|
setUnreadNotificationCount(unreadCount)
|
|
},
|
|
[notificationList],
|
|
)
|
|
|
|
// Toast
|
|
const getToastNotifications = async () => {
|
|
const resp = await getList({
|
|
channels: [NotificationChannels.UiToast],
|
|
isListRequest: false,
|
|
maxResultCount: 1000,
|
|
})
|
|
const items = resp.data.items ?? []
|
|
const newNotificationList = items.filter(
|
|
(a) => !toastNotificationList.includes(a.id) && !a.isSent,
|
|
)
|
|
setToastNotificationList(newNotificationList.map((a) => a.id))
|
|
for (const notification of newNotificationList) {
|
|
toast.push(
|
|
<Notify
|
|
type="success"
|
|
duration={0}
|
|
closable={true}
|
|
onClose={async () => {
|
|
await updateRead(notification.id, true)
|
|
}}
|
|
>
|
|
{notification.message}
|
|
</Notify>,
|
|
{
|
|
placement: 'bottom-end',
|
|
},
|
|
)
|
|
|
|
await updateSent(notification.id, true)
|
|
}
|
|
setToastNotificationList([])
|
|
}
|
|
|
|
useEffect(() => {
|
|
toastNotificationInterval.current = setInterval(async () => {
|
|
if (tabHasFocus) {
|
|
await getToastNotifications()
|
|
}
|
|
}, notificationInterval)
|
|
|
|
return () => {
|
|
clearInterval(toastNotificationInterval.current)
|
|
}
|
|
}, [toastNotificationList, tabHasFocus])
|
|
|
|
//Desktop
|
|
const getDesktopNotifications = async () => {
|
|
const resp = await getList({
|
|
channels: [NotificationChannels.Desktop],
|
|
isListRequest: false,
|
|
maxResultCount: 1000,
|
|
})
|
|
const items = resp.data.items ?? []
|
|
const newNotificationList = items.filter(
|
|
(a) => !desktopNotificationList.includes(a.id) && !a.isSent,
|
|
)
|
|
setDesktopNotificationList(newNotificationList.map((a) => a.id))
|
|
for (const notification of newNotificationList) {
|
|
var options = {
|
|
body: notification.message,
|
|
dir: 'ltr',
|
|
} as NotificationOptions
|
|
new window.Notification(notification.notificationType, options)
|
|
|
|
await updateSent(notification.id, true)
|
|
}
|
|
setDesktopNotificationList([])
|
|
}
|
|
|
|
useEffect(() => {
|
|
desktopNotificationInterval.current = setInterval(async () => {
|
|
await getDesktopNotifications()
|
|
}, notificationInterval)
|
|
|
|
return () => {
|
|
clearInterval(desktopNotificationInterval.current)
|
|
}
|
|
}, [desktopNotificationList])
|
|
|
|
return (
|
|
<Dropdown
|
|
renderTitle={
|
|
<NotificationToggle unreadCount={unreadNotificationCount} className={className} />
|
|
}
|
|
menuClass="p-0 min-w-[280px] md:min-w-[340px]"
|
|
placement={larger.md ? 'bottom-end' : 'bottom-center'}
|
|
onOpen={onNotificationOpen}
|
|
>
|
|
<Dropdown.Item variant="header">
|
|
<div className="border-b border-gray-200 dark:border-gray-600 px-4 py-2 flex items-center justify-between">
|
|
<h6>{translate('::Abp.Identity.ActivityLogs.Notifications')}</h6>
|
|
<Tooltip title={translate('::Abp.Identity.ActivityLogs.MarkAllRead')}>
|
|
<Button
|
|
variant="plain"
|
|
shape="circle"
|
|
size="sm"
|
|
icon={<HiOutlineMailOpen className="text-xl" />}
|
|
onClick={onMarkAllAsRead}
|
|
/>
|
|
</Tooltip>
|
|
</div>
|
|
</Dropdown.Item>
|
|
<div className={classNames('overflow-y-auto', notificationHeight)}>
|
|
<ScrollBar direction={direction}>
|
|
{notificationList.length > 0 &&
|
|
notificationList.map((item, index) => (
|
|
<div
|
|
key={item.id}
|
|
className={`relative flex px-4 py-4 cursor-pointer hover:bg-gray-50 active:bg-gray-100 dark:hover:bg-black dark:hover:bg-opacity-20 ${
|
|
!isLastChild(notificationList, index)
|
|
? 'border-b border-gray-200 dark:border-gray-600'
|
|
: ''
|
|
}`}
|
|
onClick={() => onMarkAsRead(item.id)}
|
|
>
|
|
<div>{notificationTypeAvatar(item.creatorId, item.tenantId)}</div>
|
|
<div className="ltr:ml-3 rtl:mr-3">
|
|
<div>
|
|
{item.notificationType && (
|
|
<span className="font-semibold heading-text">{item.notificationType} </span>
|
|
)}
|
|
<div>{item.message}</div>
|
|
</div>
|
|
<span className="text-xs">{dayjs(item.date).fromNow()}</span>
|
|
</div>
|
|
<Badge
|
|
className="absolute top-4 ltr:right-4 rtl:left-4 mt-1.5"
|
|
innerClass={`${item.readed ? 'bg-gray-300' : bgTheme} `}
|
|
/>
|
|
</div>
|
|
))}
|
|
{loading && (
|
|
<div className={classNames('flex items-center justify-center', notificationHeight)}>
|
|
<Spinner size={40} />
|
|
</div>
|
|
)}
|
|
{noResult && (
|
|
<div className={classNames('flex items-center justify-center', notificationHeight)}>
|
|
<div className="text-center">
|
|
<img
|
|
className="mx-auto mb-2 max-w-[150px]"
|
|
src="/img/others/no-notification.png"
|
|
alt="no-notification"
|
|
/>
|
|
<h6 className="font-semibold">No notifications!</h6>
|
|
<p className="mt-1">Please Try again later</p>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</ScrollBar>
|
|
</div>
|
|
<Dropdown.Item variant="header">
|
|
<div className="flex justify-center border-t border-gray-200 dark:border-gray-600 px-4 py-2">
|
|
<Link
|
|
to={ROUTES_ENUM.admin.activityLogs}
|
|
className="font-semibold cursor-pointer p-2 px-3 text-gray-600 hover:text-gray-900 dark:text-gray-200 dark:hover:text-white"
|
|
>
|
|
{translate('::Abp.Identity.ActivityLogs.ViewAllActivity')}
|
|
</Link>
|
|
</div>
|
|
</Dropdown.Item>
|
|
</Dropdown>
|
|
)
|
|
}
|
|
|
|
const Notification = withHeaderItem(_Notification)
|
|
|
|
export default Notification
|