(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol. margin.at
extension web atproto comments
98
fork

Configure Feed

Select the types of activity you want to include in your feed.

at ui-refactor 233 lines 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}