Write on the margins of the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
at main 6.9 kB view raw
1import { useState, useEffect } from "react"; 2import { Link } from "react-router-dom"; 3import { useAuth } from "../context/AuthContext"; 4import { getNotifications, markNotificationsRead } from "../api/client"; 5import { BellIcon, HeartIcon, ReplyIcon } from "../components/Icons"; 6 7function getNotificationRoute(n) { 8 if (n.type === "reply" && n.subject?.inReplyTo) { 9 return `/annotation/${encodeURIComponent(n.subject.inReplyTo)}`; 10 } 11 if (!n.subjectUri) return "/"; 12 if (n.subjectUri.includes("at.margin.bookmark")) { 13 return `/bookmarks`; 14 } 15 if (n.subjectUri.includes("at.margin.highlight")) { 16 return `/highlights`; 17 } 18 return `/annotation/${encodeURIComponent(n.subjectUri)}`; 19} 20 21export default function Notifications() { 22 const { user } = useAuth(); 23 const [notifications, setNotifications] = useState([]); 24 const [loading, setLoading] = useState(true); 25 const [error, setError] = useState(null); 26 27 useEffect(() => { 28 if (!user?.did) return; 29 30 async function load() { 31 try { 32 setLoading(true); 33 const data = await getNotifications(); 34 setNotifications(data.items || []); 35 await markNotificationsRead(); 36 } catch (err) { 37 setError(err.message); 38 } finally { 39 setLoading(false); 40 } 41 } 42 load(); 43 }, [user?.did]); 44 45 const formatTime = (dateStr) => { 46 const date = new Date(dateStr); 47 const now = new Date(); 48 const diffMs = now - date; 49 const diffMins = Math.floor(diffMs / 60000); 50 const diffHours = Math.floor(diffMs / 3600000); 51 const diffDays = Math.floor(diffMs / 86400000); 52 53 if (diffMins < 1) return "just now"; 54 if (diffMins < 60) return `${diffMins}m ago`; 55 if (diffHours < 24) return `${diffHours}h ago`; 56 if (diffDays < 7) return `${diffDays}d ago`; 57 return date.toLocaleDateString(); 58 }; 59 60 const getNotificationIcon = (type) => { 61 switch (type) { 62 case "like": 63 return <HeartIcon size={16} />; 64 case "reply": 65 return <ReplyIcon size={16} />; 66 default: 67 return <BellIcon size={16} />; 68 } 69 }; 70 71 const getNotificationText = (n) => { 72 const name = n.actor?.displayName || n.actor?.handle || "Unknown"; 73 const handle = n.actor?.handle; 74 75 switch (n.type) { 76 case "like": 77 return ( 78 <span> 79 <Link 80 to={`/profile/${handle}`} 81 className="notification-author-link" 82 onClick={(e) => e.stopPropagation()} 83 > 84 {name} 85 </Link>{" "} 86 liked your annotation 87 </span> 88 ); 89 case "reply": 90 return ( 91 <span> 92 <Link 93 to={`/profile/${handle}`} 94 className="notification-author-link" 95 onClick={(e) => e.stopPropagation()} 96 > 97 {name} 98 </Link>{" "} 99 replied to your annotation 100 </span> 101 ); 102 default: 103 return ( 104 <span> 105 <Link 106 to={`/profile/${handle}`} 107 className="notification-author-link" 108 onClick={(e) => e.stopPropagation()} 109 > 110 {name} 111 </Link>{" "} 112 interacted with your content 113 </span> 114 ); 115 } 116 }; 117 118 if (!user) { 119 return ( 120 <div className="notifications-page"> 121 <div className="page-header"> 122 <h1 className="page-title">Notifications</h1> 123 </div> 124 <div className="empty-state"> 125 <BellIcon size={48} /> 126 <h3>Sign in to see notifications</h3> 127 <p>Get notified when people like or reply to your content</p> 128 </div> 129 </div> 130 ); 131 } 132 133 return ( 134 <div className="notifications-page"> 135 <div className="page-header"> 136 <h1 className="page-title">Notifications</h1> 137 <p className="page-description"> 138 Likes and replies on your annotations 139 </p> 140 </div> 141 142 {loading && ( 143 <div className="loading-container"> 144 <div className="loading-spinner"></div> 145 </div> 146 )} 147 148 {error && ( 149 <div className="error-message"> 150 <p>Error: {error}</p> 151 </div> 152 )} 153 154 {!loading && !error && notifications.length === 0 && ( 155 <div className="empty-state"> 156 <BellIcon size={48} /> 157 <h3>No notifications yet</h3> 158 <p> 159 When someone likes or replies to your content, you&apos;ll see it 160 here 161 </p> 162 </div> 163 )} 164 165 {!loading && !error && notifications.length > 0 && ( 166 <div className="notifications-list"> 167 {notifications.map((n, i) => ( 168 <Link 169 key={n.id || i} 170 to={getNotificationRoute(n)} 171 className="notification-item card" 172 style={{ alignItems: "center" }} 173 > 174 <div 175 className="notification-avatar-container" 176 style={{ marginRight: 12, position: "relative" }} 177 > 178 {n.actor?.avatar ? ( 179 <img 180 src={n.actor.avatar} 181 alt={n.actor.handle} 182 style={{ 183 width: 40, 184 height: 40, 185 borderRadius: "50%", 186 objectFit: "cover", 187 }} 188 /> 189 ) : ( 190 <div 191 style={{ 192 width: 40, 193 height: 40, 194 borderRadius: "50%", 195 background: "#eee", 196 display: "flex", 197 alignItems: "center", 198 justifyContent: "center", 199 }} 200 > 201 {(n.actor?.handle || "?")[0].toUpperCase()} 202 </div> 203 )} 204 <div 205 className="notification-icon-badge" 206 data-type={n.type} 207 style={{ 208 position: "absolute", 209 bottom: -4, 210 right: -4, 211 background: "var(--bg-primary)", 212 borderRadius: "50%", 213 padding: 2, 214 display: "flex", 215 boxShadow: "0 2px 4px rgba(0,0,0,0.1)", 216 }} 217 > 218 {getNotificationIcon(n.type)} 219 </div> 220 </div> 221 <div className="notification-content"> 222 <p className="notification-text">{getNotificationText(n)}</p> 223 <span className="notification-time"> 224 {formatTime(n.createdAt)} 225 </span> 226 </div> 227 </Link> 228 ))} 229 </div> 230 )} 231 </div> 232 ); 233}