import React, { useEffect, useState } from "react"; import { Link } from "react-router-dom"; import { getNotifications, markNotificationsRead } from "../../api/client"; import type { NotificationItem, AnnotationItem } from "../../types"; import { Heart, MessageCircle, Bell, PenTool, Bookmark, UserPlus, AtSign, ExternalLink, } from "lucide-react"; import { formatDistanceToNow } from "date-fns"; import { clsx } from "clsx"; import { Avatar, EmptyState, Skeleton } from "../../components/ui"; function getContentType( uri: string, ): "annotation" | "highlight" | "bookmark" | "reply" | "unknown" { if (uri.includes("/at.margin.annotation/")) return "annotation"; if (uri.includes("/at.margin.highlight/")) return "highlight"; if (uri.includes("/at.margin.bookmark/")) return "bookmark"; if (uri.includes("/at.margin.reply/")) return "reply"; return "unknown"; } function getNotificationVerb( notifType: string, contentType: string, subject?: AnnotationItem, ): string { switch (notifType) { case "like": switch (contentType) { case "annotation": return "liked your annotation"; case "highlight": return "liked your highlight"; case "bookmark": return "liked your bookmark"; case "reply": return "liked your reply"; default: return "liked your post"; } case "reply": { const parentUri = subject?.inReplyTo; const parentIsReply = parentUri ? getContentType(parentUri) === "reply" : false; return parentIsReply ? "replied to your reply" : "replied to your annotation"; } case "mention": return "mentioned you in an annotation"; case "follow": return "followed you"; case "highlight": return "highlighted your page"; default: return notifType; } } const NotificationIcon = ({ type }: { type: string }) => { const base = "p-2 rounded-full"; switch (type) { case "like": return (
); case "reply": return (
); case "highlight": return (
); case "bookmark": return (
); case "follow": return (
); case "mention": return (
); default: return (
); } }; function SubjectPreview({ subject, subjectUri, }: { subject: AnnotationItem | unknown; subjectUri: string; }) { const item = subject as AnnotationItem | undefined; if (!item?.uri && !subjectUri) return null; const contentType = getContentType(subjectUri); const href = `/annotation/${encodeURIComponent(subjectUri)}`; let preview: React.ReactNode = null; if (contentType === "annotation") { const quote = item?.target?.selector?.exact; const body = item?.text || item?.body?.value; preview = ( <> {quote && (

“{quote}”

)} {body && (

{body}

)} ); } else if (contentType === "highlight") { const quote = item?.target?.selector?.exact; preview = quote ? (

“{quote}”

) : null; } else if (contentType === "bookmark") { const title = item?.title || item?.target?.title; const source = item?.source || item?.target?.source; preview = ( <> {title && (

{title}

)} {source && (

{(() => { try { return new URL(source).hostname; } catch { return source; } })()}

)} ); } else if (contentType === "reply") { const text = item?.text; const parentUri = item?.inReplyTo; const parentIsReply = parentUri ? getContentType(parentUri) === "reply" : false; preview = ( <> {text && (

{text}

)} {parentUri && (

in reply to{" "} e.stopPropagation()} > {parentIsReply ? "a reply" : "an annotation"}

)} ); } if (!preview) return null; return ( {preview} ); } export default function Notifications() { const [notifications, setNotifications] = useState([]); const [loading, setLoading] = useState(true); useEffect(() => { const load = async () => { setLoading(true); const data = await getNotifications(); setNotifications(data); setLoading(false); markNotificationsRead(); }; load(); }, []); if (loading) { return (

Activity

{[1, 2, 3].map((i) => (
))}
); } if (notifications.length === 0) { return (

Activity

} title="No activity yet" message="Interactions with your content will appear here." />
); } return (

Activity

{notifications.map((n) => { const contentType = getContentType(n.subjectUri || ""); const verb = getNotificationVerb( n.type, contentType, n.subject as AnnotationItem, ); const timeAgo = formatDistanceToNow(new Date(n.createdAt), { addSuffix: false, }); return (
{n.actor.displayName || `@${n.actor.handle}`} {" "} {n.type !== "follow" && n.subjectUri ? ( {verb} ) : ( verb )} {timeAgo}
{n.subject !== undefined && n.subject !== null && ( )}
); })}
); }