feat: Show notice on new notifications (#8390)
This commit is contained in:
+79
-17
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
BellAlert,
|
BellAlert,
|
||||||
BellAlertDone,
|
BellAlertDone,
|
||||||
|
CircleFilledSolid,
|
||||||
InformationCircleSolid,
|
InformationCircleSolid,
|
||||||
} from "@medusajs/icons"
|
} from "@medusajs/icons"
|
||||||
import { Drawer, Heading, IconButton, Text } from "@medusajs/ui"
|
import { Drawer, Heading, IconButton, Text } from "@medusajs/ui"
|
||||||
@@ -10,7 +11,7 @@ import { HttpTypes } from "@medusajs/types"
|
|||||||
import { formatDistance } from "date-fns"
|
import { formatDistance } from "date-fns"
|
||||||
import { InfiniteList } from "../../common/infinite-list"
|
import { InfiniteList } from "../../common/infinite-list"
|
||||||
import { sdk } from "../../../lib/client"
|
import { sdk } from "../../../lib/client"
|
||||||
import { notificationQueryKeys } from "../../../hooks/api"
|
import { notificationQueryKeys, useNotifications } from "../../../hooks/api"
|
||||||
import { TFunction } from "i18next"
|
import { TFunction } from "i18next"
|
||||||
import { FilePreview } from "../../common/file-preview"
|
import { FilePreview } from "../../common/file-preview"
|
||||||
|
|
||||||
@@ -24,9 +25,17 @@ interface NotificationData {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const LAST_READ_NOTIFICATION_KEY = "notificationsLastReadAt"
|
||||||
|
|
||||||
export const Notifications = () => {
|
export const Notifications = () => {
|
||||||
const [open, setOpen] = useState(false)
|
|
||||||
const { t } = useTranslation()
|
const { t } = useTranslation()
|
||||||
|
const [open, setOpen] = useState(false)
|
||||||
|
const [hasUnread, setHasUnread] = useUnreadNotifications()
|
||||||
|
// This is used to show the unread icon on the notification when the drawer is open,
|
||||||
|
// so it should lag behind the local storage data and should only be reset on close
|
||||||
|
const [lastReadAt, setLastReadAt] = useState(
|
||||||
|
localStorage.getItem(LAST_READ_NOTIFICATION_KEY)
|
||||||
|
)
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const onKeyDown = (e: KeyboardEvent) => {
|
const onKeyDown = (e: KeyboardEvent) => {
|
||||||
@@ -42,14 +51,25 @@ export const Notifications = () => {
|
|||||||
}
|
}
|
||||||
}, [])
|
}, [])
|
||||||
|
|
||||||
|
const handleOnOpen = (isOpen: boolean) => {
|
||||||
|
if (isOpen) {
|
||||||
|
setHasUnread(false)
|
||||||
|
setOpen(true)
|
||||||
|
localStorage.setItem(LAST_READ_NOTIFICATION_KEY, new Date().toISOString())
|
||||||
|
} else {
|
||||||
|
setOpen(false)
|
||||||
|
setLastReadAt(localStorage.getItem(LAST_READ_NOTIFICATION_KEY))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Drawer open={open} onOpenChange={setOpen}>
|
<Drawer open={open} onOpenChange={handleOnOpen}>
|
||||||
<Drawer.Trigger asChild>
|
<Drawer.Trigger asChild>
|
||||||
<IconButton
|
<IconButton
|
||||||
variant="transparent"
|
variant="transparent"
|
||||||
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
className="text-ui-fg-muted hover:text-ui-fg-subtle"
|
||||||
>
|
>
|
||||||
<BellAlert />
|
{hasUnread ? <BellAlertDone /> : <BellAlert />}
|
||||||
</IconButton>
|
</IconButton>
|
||||||
</Drawer.Trigger>
|
</Drawer.Trigger>
|
||||||
<Drawer.Content>
|
<Drawer.Content>
|
||||||
@@ -77,6 +97,10 @@ export const Notifications = () => {
|
|||||||
<Notification
|
<Notification
|
||||||
key={notification.id}
|
key={notification.id}
|
||||||
notification={notification}
|
notification={notification}
|
||||||
|
unread={
|
||||||
|
Date.parse(notification.created_at) >
|
||||||
|
(lastReadAt ? Date.parse(lastReadAt) : 0)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)
|
)
|
||||||
}}
|
}}
|
||||||
@@ -89,8 +113,10 @@ export const Notifications = () => {
|
|||||||
|
|
||||||
const Notification = ({
|
const Notification = ({
|
||||||
notification,
|
notification,
|
||||||
|
unread,
|
||||||
}: {
|
}: {
|
||||||
notification: HttpTypes.AdminNotification
|
notification: HttpTypes.AdminNotification
|
||||||
|
unread?: boolean
|
||||||
}) => {
|
}) => {
|
||||||
const data = notification.data as unknown as NotificationData | undefined
|
const data = notification.data as unknown as NotificationData | undefined
|
||||||
|
|
||||||
@@ -101,27 +127,35 @@ const Notification = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="flex items-start justify-center gap-3 border-b p-6">
|
<div className="flex items-start justify-center gap-3 border-b p-6 relative">
|
||||||
<div className="text-ui-fg-muted flex size-5 items-center justify-center">
|
<div className="text-ui-fg-muted flex size-5 items-center justify-center">
|
||||||
<InformationCircleSolid />
|
<InformationCircleSolid />
|
||||||
</div>
|
</div>
|
||||||
<div className="flex w-full flex-col gap-y-3">
|
<div className="flex w-full flex-col gap-y-3">
|
||||||
<div>
|
<div>
|
||||||
<div className="align-center flex flex-row justify-between">
|
<div className="items-center flex flex-row justify-between">
|
||||||
<Text size="small" leading="compact" weight="plus">
|
<Text size="small" leading="compact" weight="plus">
|
||||||
{data.title}
|
{data.title}
|
||||||
</Text>
|
</Text>
|
||||||
<Text
|
<div className="items-center flex flex-row justify-center pr-4 relative">
|
||||||
as={"span"}
|
<Text
|
||||||
className="text-ui-fg-subtle"
|
as={"span"}
|
||||||
size="small"
|
className="text-ui-fg-subtle"
|
||||||
leading="compact"
|
size="small"
|
||||||
weight="plus"
|
leading="compact"
|
||||||
>
|
weight="plus"
|
||||||
{formatDistance(notification.created_at, new Date(), {
|
>
|
||||||
addSuffix: true,
|
{formatDistance(notification.created_at, new Date(), {
|
||||||
})}
|
addSuffix: true,
|
||||||
</Text>
|
})}
|
||||||
|
</Text>
|
||||||
|
{unread && (
|
||||||
|
<CircleFilledSolid
|
||||||
|
className="text-ui-fg-interactive absolute top-1 right-0"
|
||||||
|
style={{ transform: "scale(0.3)" }}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{!!data.description && (
|
{!!data.description && (
|
||||||
<Text
|
<Text
|
||||||
@@ -161,3 +195,31 @@ const NotificationsEmptyState = ({ t }: { t: TFunction }) => {
|
|||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const useUnreadNotifications = () => {
|
||||||
|
const [hasUnread, setHasUnread] = useState(false)
|
||||||
|
const { notifications } = useNotifications(
|
||||||
|
{ limit: 1, offset: 0, fields: "created_at" },
|
||||||
|
{ refetchInterval: 3000 }
|
||||||
|
)
|
||||||
|
const lastNotification = notifications?.[0]
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!lastNotification) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const lastNotificationAsTimestamp = Date.parse(lastNotification.created_at)
|
||||||
|
|
||||||
|
const lastReadDatetime = localStorage.getItem(LAST_READ_NOTIFICATION_KEY)
|
||||||
|
const lastReadAsTimestamp = lastReadDatetime
|
||||||
|
? Date.parse(lastReadDatetime)
|
||||||
|
: 0
|
||||||
|
|
||||||
|
if (lastNotificationAsTimestamp > lastReadAsTimestamp) {
|
||||||
|
setHasUnread(true)
|
||||||
|
}
|
||||||
|
}, [lastNotification])
|
||||||
|
|
||||||
|
return [hasUnread, setHasUnread] as const
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,5 +11,5 @@ export interface AdminNotification {
|
|||||||
original_notification_id?: string | null
|
original_notification_id?: string | null
|
||||||
external_id?: string | null
|
external_id?: string | null
|
||||||
provider_id: string
|
provider_id: string
|
||||||
created_at: Date
|
created_at: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user