import React, { useEffect, useRef } from "react"; import Image from "next/image"; import Link from "next/link"; import { useRouter } from "next/router"; import { ArchiveRestore, Clock, MessageSquare, MoreVertical, User2 } from "lucide-react"; import { Menu } from "@headlessui/react"; // type import type { IUserNotification, NotificationType } from "@plane/types"; // ui import { ArchiveIcon, CustomMenu, Tooltip, TOAST_TYPE, setToast } from "@plane/ui"; // constants import { ISSUE_OPENED, NOTIFICATIONS_READ, NOTIFICATION_ARCHIVED, NOTIFICATION_SNOOZED, } from "@/constants/event-tracker"; import { snoozeOptions } from "@/constants/notification"; // helper import { calculateTimeAgo, renderFormattedTime, renderFormattedDate, getDate } from "@/helpers/date-time.helper"; import { replaceUnderscoreIfSnakeCase, truncateText, stripAndTruncateHTML } from "@/helpers/string.helper"; // hooks import { useEventTracker } from "@/hooks/store"; import { usePlatformOS } from "@/hooks/use-platform-os"; type NotificationCardProps = { selectedTab: NotificationType; notification: IUserNotification; isSnoozedTabOpen: boolean; closePopover: () => void; markNotificationReadStatus: (notificationId: string) => Promise; markNotificationReadStatusToggle: (notificationId: string) => Promise; markNotificationArchivedStatus: (notificationId: string) => Promise; setSelectedNotificationForSnooze: (notificationId: string) => void; markSnoozeNotification: (notificationId: string, dateTime?: Date | undefined) => Promise; }; export const NotificationCard: React.FC = (props) => { const { selectedTab, notification, isSnoozedTabOpen, closePopover, markNotificationReadStatus, markNotificationReadStatusToggle, markNotificationArchivedStatus, setSelectedNotificationForSnooze, markSnoozeNotification, } = props; // store hooks const { captureEvent } = useEventTracker(); const { isMobile } = usePlatformOS(); const router = useRouter(); const { workspaceSlug } = router.query; // states const [showSnoozeOptions, setShowSnoozeOptions] = React.useState(false); // refs const snoozeRef = useRef(null); const moreOptions = [ { id: 1, name: notification.read_at ? "Mark as unread" : "Mark as read", icon: , onClick: () => { markNotificationReadStatusToggle(notification.id).then(() => { setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", type: TOAST_TYPE.SUCCESS, }); }); }, }, { id: 2, name: notification.archived_at ? "Unarchive" : "Archive", icon: notification.archived_at ? ( ) : ( ), onClick: () => { markNotificationArchivedStatus(notification.id).then(() => { setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", type: TOAST_TYPE.SUCCESS, }); }); }, }, ]; const snoozeOptionOnClick = (date: Date | null) => { if (!date) { setSelectedNotificationForSnooze(notification.id); return; } markSnoozeNotification(notification.id, date).then(() => { setToast({ title: `Notification snoozed till ${renderFormattedDate(date)}`, type: TOAST_TYPE.SUCCESS, }); }); }; // close snooze options on outside click useEffect(() => { const handleClickOutside = (event: any) => { if (snoozeRef.current && !snoozeRef.current?.contains(event.target)) { setShowSnoozeOptions(false); } }; document.addEventListener("mousedown", handleClickOutside, true); document.addEventListener("touchend", handleClickOutside, true); return () => { document.removeEventListener("mousedown", handleClickOutside, true); document.removeEventListener("touchend", handleClickOutside, true); }; }, []); const notificationField = notification.data.issue_activity.field; const notificationTriggeredBy = notification.triggered_by_details; const snoozedTillDate = getDate(notification?.snoozed_till); if (snoozedTillDate && isSnoozedTabOpen && snoozedTillDate < new Date()) return null; return ( { markNotificationReadStatus(notification.id); captureEvent(ISSUE_OPENED, { issue_id: notification.data.issue.id, element: "notification", }); closePopover(); }} href={`/${workspaceSlug}/projects/${notification.project}/${ notificationField === "archived_at" ? "archives/" : "" }issues/${notification.data.issue.id}`} className={`group relative flex w-full cursor-pointer items-center gap-4 p-3 pl-6 ${ notification.read_at === null ? "bg-custom-primary-70/5" : "hover:bg-custom-background-200" }`} > {notification.read_at === null && ( )}
{notificationTriggeredBy.avatar && notificationTriggeredBy.avatar !== "" ? (
Profile Image
) : (
{notificationTriggeredBy.is_bot ? ( notificationTriggeredBy.first_name?.[0]?.toUpperCase() ) : notificationTriggeredBy.display_name?.[0] ? ( notificationTriggeredBy.display_name?.[0]?.toUpperCase() ) : ( )}
)}
{!notification.message ? (
{notificationTriggeredBy.is_bot ? notificationTriggeredBy.first_name : notificationTriggeredBy.display_name}{" "} {!["comment", "archived_at"].includes(notificationField) && notification.data.issue_activity.verb}{" "} {notificationField === "comment" ? "commented" : notificationField === "archived_at" ? notification.data.issue_activity.new_value === "restore" ? "restored the issue" : "archived the issue" : notificationField === "None" ? null : replaceUnderscoreIfSnakeCase(notificationField)}{" "} {!["comment", "archived_at", "None"].includes(notificationField) ? "to" : ""} {" "} {notificationField !== "None" ? ( notificationField !== "comment" ? ( notificationField === "target_date" ? ( renderFormattedDate(notification.data.issue_activity.new_value) ) : notificationField === "attachment" ? ( "the issue" ) : notificationField === "description" ? ( stripAndTruncateHTML(notification.data.issue_activity.new_value, 55) ) : notificationField === "archived_at" ? null : ( notification.data.issue_activity.new_value ) ) : ( {`"`} {notification.data.issue_activity.new_value.length > 55 ? notification?.data?.issue_activity?.issue_comment?.slice(0, 50) + "..." : notification.data.issue_activity.issue_comment} {`"`} ) ) : ( "the issue and assigned it to you." )}
) : (
{notification.message}
)}
{({ open }) => ( <> {open && (
{moreOptions.map((item) => ( {({ close }) => ( )} ))}
{ e.stopPropagation(); e.preventDefault(); setShowSnoozeOptions(true); }} className="flex items-center gap-x-2 p-1.5" > Snooze
)} )}
{showSnoozeOptions && (
{snoozeOptions.map((item) => (

{ e.stopPropagation(); e.preventDefault(); setShowSnoozeOptions(false); snoozeOptionOnClick(item.value); }} > {item.label}

))}
)}

{truncateText( `${notification.data.issue.identifier}-${notification.data.issue.sequence_id} ${notification.data.issue.name}`, 50 )}

{notification.snoozed_till ? (

Till {renderFormattedDate(notification.snoozed_till)},{" "} {renderFormattedTime(notification.snoozed_till, "12-hour")}

) : (

{calculateTimeAgo(notification.created_at)}

)}
{[ { id: 1, name: notification.read_at ? "Mark as unread" : "Mark as read", icon: , onClick: () => { markNotificationReadStatusToggle(notification.id).then(() => { captureEvent(NOTIFICATIONS_READ, { issue_id: notification.data.issue.id, tab: selectedTab, state: "SUCCESS", }); setToast({ title: notification.read_at ? "Notification marked as read" : "Notification marked as unread", type: TOAST_TYPE.SUCCESS, }); }); }, }, { id: 2, name: notification.archived_at ? "Unarchive" : "Archive", icon: notification.archived_at ? ( ) : ( ), onClick: () => { markNotificationArchivedStatus(notification.id).then(() => { captureEvent(NOTIFICATION_ARCHIVED, { issue_id: notification.data.issue.id, tab: selectedTab, state: "SUCCESS", }); setToast({ title: notification.archived_at ? "Notification un-archived" : "Notification archived", type: TOAST_TYPE.SUCCESS, }); }); }, }, ].map((item) => ( ))}
} optionsClassName="!z-20" > {snoozeOptions.map((item) => ( { e.stopPropagation(); e.preventDefault(); if (!item.value) { setSelectedNotificationForSnooze(notification.id); return; } markSnoozeNotification(notification.id, item.value).then(() => { captureEvent(NOTIFICATION_SNOOZED, { issue_id: notification.data.issue.id, tab: selectedTab, state: "SUCCESS", }); setToast({ title: `Notification snoozed till ${renderFormattedDate(item.value)}`, type: TOAST_TYPE.SUCCESS, }); }); }} > {item.label} ))}
); };