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

Configure Feed

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

at ui-refactor 509 lines 14 kB view raw
1const API_BASE = "/api"; 2const AUTH_BASE = "/auth"; 3 4async function request(endpoint, options = {}) { 5 const response = await fetch(endpoint, { 6 credentials: "include", 7 headers: { 8 "Content-Type": "application/json", 9 ...options.headers, 10 }, 11 ...options, 12 }); 13 14 if (!response.ok) { 15 const error = await response.text(); 16 throw new Error(error || `HTTP ${response.status}`); 17 } 18 19 return response.json(); 20} 21 22export async function getURLMetadata(url) { 23 return request(`${API_BASE}/url-metadata?url=${encodeURIComponent(url)}`); 24} 25 26export async function getAnnotationFeed( 27 limit = 50, 28 offset = 0, 29 tag = "", 30 creator = "", 31 feedType = "", 32 motivation = "", 33) { 34 let url = `${API_BASE}/annotations/feed?limit=${limit}&offset=${offset}`; 35 if (tag) url += `&tag=${encodeURIComponent(tag)}`; 36 if (creator) url += `&creator=${encodeURIComponent(creator)}`; 37 if (feedType) url += `&type=${encodeURIComponent(feedType)}`; 38 if (motivation) url += `&motivation=${encodeURIComponent(motivation)}`; 39 return request(url); 40} 41 42export async function getAnnotations({ 43 source, 44 motivation, 45 limit = 50, 46 offset = 0, 47} = {}) { 48 let url = `${API_BASE}/annotations?limit=${limit}&offset=${offset}`; 49 if (source) url += `&source=${encodeURIComponent(source)}`; 50 if (motivation) url += `&motivation=${motivation}`; 51 return request(url); 52} 53 54export async function getByTarget(source, limit = 50, offset = 0) { 55 return request( 56 `${API_BASE}/targets?source=${encodeURIComponent(source)}&limit=${limit}&offset=${offset}`, 57 ); 58} 59 60export async function getAnnotation(uri) { 61 return request(`${API_BASE}/annotation?uri=${encodeURIComponent(uri)}`); 62} 63 64export async function getProfile(did) { 65 return request(`${API_BASE}/profile/${encodeURIComponent(did)}`); 66} 67 68export async function getUserAnnotations(did, limit = 50, offset = 0) { 69 return request( 70 `${API_BASE}/users/${encodeURIComponent(did)}/annotations?limit=${limit}&offset=${offset}`, 71 ); 72} 73 74export async function getUserHighlights(did, limit = 50, offset = 0) { 75 return request( 76 `${API_BASE}/users/${encodeURIComponent(did)}/highlights?limit=${limit}&offset=${offset}`, 77 ); 78} 79 80export async function getUserBookmarks(did, limit = 50, offset = 0) { 81 return request( 82 `${API_BASE}/users/${encodeURIComponent(did)}/bookmarks?limit=${limit}&offset=${offset}`, 83 ); 84} 85 86export async function getUserTargetItems(did, url, limit = 50, offset = 0) { 87 return request( 88 `${API_BASE}/users/${encodeURIComponent(did)}/targets?source=${encodeURIComponent(url)}&limit=${limit}&offset=${offset}`, 89 ); 90} 91 92export async function getHighlights(creatorDid, limit = 50, offset = 0) { 93 return request( 94 `${API_BASE}/highlights?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, 95 ); 96} 97 98export async function getBookmarks(creatorDid, limit = 50, offset = 0) { 99 return request( 100 `${API_BASE}/bookmarks?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, 101 ); 102} 103 104export async function getReplies(annotationUri) { 105 return request( 106 `${API_BASE}/replies?uri=${encodeURIComponent(annotationUri)}`, 107 ); 108} 109 110export async function updateAnnotation(uri, text, tags) { 111 return request(`${API_BASE}/annotations?uri=${encodeURIComponent(uri)}`, { 112 method: "PUT", 113 body: JSON.stringify({ text, tags }), 114 }); 115} 116 117export async function updateHighlight(uri, color, tags) { 118 return request(`${API_BASE}/highlights?uri=${encodeURIComponent(uri)}`, { 119 method: "PUT", 120 body: JSON.stringify({ color, tags }), 121 }); 122} 123 124export async function createBookmark(url, title, description) { 125 return request(`${API_BASE}/bookmarks`, { 126 method: "POST", 127 body: JSON.stringify({ url, title, description }), 128 }); 129} 130 131export async function updateBookmark(uri, title, description, tags) { 132 return request(`${API_BASE}/bookmarks?uri=${encodeURIComponent(uri)}`, { 133 method: "PUT", 134 body: JSON.stringify({ title, description, tags }), 135 }); 136} 137 138export async function getCollections(did) { 139 let url = `${API_BASE}/collections`; 140 if (did) url += `?author=${encodeURIComponent(did)}`; 141 return request(url); 142} 143 144export async function getCollection(uri) { 145 return request(`${API_BASE}/collection?uri=${encodeURIComponent(uri)}`); 146} 147 148export async function getCollectionsContaining(annotationUri) { 149 return request( 150 `${API_BASE}/collections/containing?uri=${encodeURIComponent(annotationUri)}`, 151 ); 152} 153 154export async function getEditHistory(uri) { 155 return request( 156 `${API_BASE}/annotations/history?uri=${encodeURIComponent(uri)}`, 157 ); 158} 159 160export async function getNotifications(limit = 50, offset = 0) { 161 return request(`${API_BASE}/notifications?limit=${limit}&offset=${offset}`); 162} 163 164export async function getUnreadNotificationCount() { 165 return request(`${API_BASE}/notifications/count`); 166} 167 168export async function markNotificationsRead() { 169 return request(`${API_BASE}/notifications/read`, { method: "POST" }); 170} 171 172export async function updateCollection(uri, name, description, icon) { 173 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { 174 method: "PUT", 175 body: JSON.stringify({ name, description, icon }), 176 }); 177} 178 179export async function updateProfile({ bio, website, links }) { 180 return request(`${API_BASE}/profile`, { 181 method: "PUT", 182 body: JSON.stringify({ bio, website, links }), 183 }); 184} 185 186export async function createCollection(name, description, icon) { 187 return request(`${API_BASE}/collections`, { 188 method: "POST", 189 body: JSON.stringify({ name, description, icon }), 190 }); 191} 192 193export async function deleteCollection(uri) { 194 return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { 195 method: "DELETE", 196 }); 197} 198 199export async function getCollectionItems(collectionUri) { 200 return request( 201 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, 202 ); 203} 204 205export async function addItemToCollection( 206 collectionUri, 207 annotationUri, 208 position = 0, 209) { 210 return request( 211 `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, 212 { 213 method: "POST", 214 body: JSON.stringify({ annotationUri, position }), 215 }, 216 ); 217} 218 219export async function removeItemFromCollection(itemUri) { 220 return request( 221 `${API_BASE}/collections/items?uri=${encodeURIComponent(itemUri)}`, 222 { 223 method: "DELETE", 224 }, 225 ); 226} 227 228export async function getLikeCount(annotationUri) { 229 return request(`${API_BASE}/likes?uri=${encodeURIComponent(annotationUri)}`); 230} 231 232export async function deleteHighlight(rkey) { 233 return request(`${API_BASE}/highlights?rkey=${encodeURIComponent(rkey)}`, { 234 method: "DELETE", 235 }); 236} 237 238export async function deleteBookmark(rkey) { 239 return request(`${API_BASE}/bookmarks?rkey=${encodeURIComponent(rkey)}`, { 240 method: "DELETE", 241 }); 242} 243 244export async function createHighlight({ url, title, selector, color, tags }) { 245 return request(`${API_BASE}/highlights`, { 246 method: "POST", 247 body: JSON.stringify({ url, title, selector, color, tags }), 248 }); 249} 250 251export async function createAnnotation({ 252 url, 253 text, 254 quote, 255 title, 256 selector, 257 tags, 258}) { 259 return request(`${API_BASE}/annotations`, { 260 method: "POST", 261 body: JSON.stringify({ url, text, quote, title, selector, tags }), 262 }); 263} 264 265export async function deleteAnnotation(rkey, type = "annotation") { 266 return request( 267 `${API_BASE}/annotations?rkey=${encodeURIComponent(rkey)}&type=${encodeURIComponent(type)}`, 268 { 269 method: "DELETE", 270 }, 271 ); 272} 273 274export async function likeAnnotation(subjectUri, subjectCid) { 275 return request(`${API_BASE}/annotations/like`, { 276 method: "POST", 277 headers: { 278 "Content-Type": "application/json", 279 }, 280 body: JSON.stringify({ 281 subjectUri, 282 subjectCid, 283 }), 284 }); 285} 286 287export async function unlikeAnnotation(subjectUri) { 288 return request( 289 `${API_BASE}/annotations/like?uri=${encodeURIComponent(subjectUri)}`, 290 { 291 method: "DELETE", 292 }, 293 ); 294} 295 296export async function createReply({ 297 parentUri, 298 parentCid, 299 rootUri, 300 rootCid, 301 text, 302}) { 303 return request(`${API_BASE}/annotations/reply`, { 304 method: "POST", 305 body: JSON.stringify({ parentUri, parentCid, rootUri, rootCid, text }), 306 }); 307} 308 309export async function deleteReply(uri) { 310 return request( 311 `${API_BASE}/annotations/reply?uri=${encodeURIComponent(uri)}`, 312 { 313 method: "DELETE", 314 }, 315 ); 316} 317 318export async function getSession() { 319 return request(`${AUTH_BASE}/session`); 320} 321 322export async function logout() { 323 return request(`${AUTH_BASE}/logout`, { method: "POST" }); 324} 325 326export function normalizeAnnotation(item) { 327 if (!item) return {}; 328 329 if (item.type === "Annotation") { 330 return { 331 type: item.type, 332 uri: item.uri || item.id, 333 author: item.author || item.creator, 334 url: item.url || item.target?.source, 335 title: item.title || item.target?.title, 336 text: item.text || item.body?.value, 337 selector: item.selector || item.target?.selector, 338 motivation: item.motivation, 339 tags: item.tags || [], 340 createdAt: item.createdAt || item.created, 341 cid: item.cid || item.CID, 342 likeCount: item.likeCount || 0, 343 replyCount: item.replyCount || 0, 344 viewerHasLiked: item.viewerHasLiked || false, 345 }; 346 } 347 348 if (item.type === "Bookmark") { 349 return { 350 type: item.type, 351 uri: item.uri || item.id, 352 author: item.author || item.creator, 353 url: item.url || item.source, 354 title: item.title, 355 description: item.description, 356 tags: item.tags || [], 357 createdAt: item.createdAt || item.created, 358 cid: item.cid || item.CID, 359 likeCount: item.likeCount || 0, 360 replyCount: item.replyCount || 0, 361 viewerHasLiked: item.viewerHasLiked || false, 362 }; 363 } 364 365 if (item.type === "Highlight") { 366 return { 367 type: item.type, 368 uri: item.uri || item.id, 369 author: item.author || item.creator, 370 url: item.url || item.target?.source, 371 title: item.title || item.target?.title, 372 selector: item.selector || item.target?.selector, 373 color: item.color, 374 tags: item.tags || [], 375 createdAt: item.createdAt || item.created, 376 cid: item.cid || item.CID, 377 likeCount: item.likeCount || 0, 378 replyCount: item.replyCount || 0, 379 viewerHasLiked: item.viewerHasLiked || false, 380 }; 381 } 382 383 return { 384 uri: item.uri || item.id, 385 author: item.author || item.creator, 386 url: item.url || item.source || item.target?.source, 387 title: item.title || item.target?.title, 388 text: item.text || item.body?.value, 389 description: item.description, 390 selector: item.selector || item.target?.selector, 391 color: item.color, 392 tags: item.tags || [], 393 createdAt: item.createdAt || item.created, 394 cid: item.cid || item.CID, 395 likeCount: item.likeCount || 0, 396 replyCount: item.replyCount || 0, 397 viewerHasLiked: item.viewerHasLiked || false, 398 }; 399} 400 401export function normalizeHighlight(highlight) { 402 return { 403 uri: highlight.uri || highlight.id, 404 author: highlight.author || highlight.creator, 405 url: highlight.url || highlight.target?.source, 406 title: highlight.title || highlight.target?.title, 407 selector: highlight.selector || highlight.target?.selector, 408 color: highlight.color, 409 tags: highlight.tags || [], 410 createdAt: highlight.createdAt || highlight.created, 411 likeCount: highlight.likeCount || 0, 412 replyCount: highlight.replyCount || 0, 413 viewerHasLiked: highlight.viewerHasLiked || false, 414 }; 415} 416 417export function normalizeBookmark(bookmark) { 418 return { 419 uri: bookmark.uri || bookmark.id, 420 author: bookmark.author || bookmark.creator, 421 url: bookmark.url || bookmark.source, 422 title: bookmark.title, 423 description: bookmark.description, 424 tags: bookmark.tags || [], 425 createdAt: bookmark.createdAt || bookmark.created, 426 likeCount: bookmark.likeCount || 0, 427 replyCount: bookmark.replyCount || 0, 428 viewerHasLiked: bookmark.viewerHasLiked || false, 429 }; 430} 431 432export async function searchActors(query) { 433 const res = await fetch( 434 `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5`, 435 ); 436 if (!res.ok) throw new Error("Search failed"); 437 return res.json(); 438} 439 440export async function resolveHandle(handle) { 441 const res = await fetch( 442 `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, 443 ); 444 if (!res.ok) throw new Error("Failed to resolve handle"); 445 const data = await res.json(); 446 return data.did; 447} 448 449export async function startLogin(handle, inviteCode) { 450 return request(`${AUTH_BASE}/start`, { 451 method: "POST", 452 body: JSON.stringify({ handle, invite_code: inviteCode }), 453 }); 454} 455 456export async function startSignup(pdsUrl) { 457 return request(`${AUTH_BASE}/signup`, { 458 method: "POST", 459 body: JSON.stringify({ pds_url: pdsUrl }), 460 }); 461} 462export async function getTrendingTags(limit = 10) { 463 return request(`${API_BASE}/tags/trending?limit=${limit}`); 464} 465 466export async function getAPIKeys() { 467 return request(`${API_BASE}/keys`); 468} 469 470export async function createAPIKey(name) { 471 return request(`${API_BASE}/keys`, { 472 method: "POST", 473 body: JSON.stringify({ name }), 474 }); 475} 476 477export async function deleteAPIKey(id) { 478 return request(`${API_BASE}/keys/${id}`, { method: "DELETE" }); 479} 480 481export async function describeServer(service) { 482 const res = await fetch(`${service}/xrpc/com.atproto.server.describeServer`); 483 if (!res.ok) throw new Error("Failed to describe server"); 484 return res.json(); 485} 486 487export async function createAccount( 488 service, 489 { handle, email, password, inviteCode }, 490) { 491 const res = await fetch(`${service}/xrpc/com.atproto.server.createAccount`, { 492 method: "POST", 493 headers: { 494 "Content-Type": "application/json", 495 }, 496 body: JSON.stringify({ 497 handle, 498 email, 499 password, 500 inviteCode, 501 }), 502 }); 503 504 const data = await res.json(); 505 if (!res.ok) { 506 throw new Error(data.message || data.error || "Failed to create account"); 507 } 508 return data; 509}