Something went wrong. Try again.
(READ ONLY) Margin is an open annotation layer for the internet. Powered by the AT Protocol.
margin.at
extension web atproto comments
Something went wrong. Try again.
JavaScript
at ui-refactor
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510const API_BASE = "/api";const AUTH_BASE = "/auth";
async function request(endpoint, options = {}) { const response = await fetch(endpoint, { credentials: "include", headers: { "Content-Type": "application/json", ...options.headers, }, ...options, });
if (!response.ok) { const error = await response.text(); throw new Error(error || `HTTP ${response.status}`); }
return response.json();}
export async function getURLMetadata(url) { return request(`${API_BASE}/url-metadata?url=${encodeURIComponent(url)}`);}
export async function getAnnotationFeed( limit = 50, offset = 0, tag = "", creator = "", feedType = "", motivation = "",) { let url = `${API_BASE}/annotations/feed?limit=${limit}&offset=${offset}`; if (tag) url += `&tag=${encodeURIComponent(tag)}`; if (creator) url += `&creator=${encodeURIComponent(creator)}`; if (feedType) url += `&type=${encodeURIComponent(feedType)}`; if (motivation) url += `&motivation=${encodeURIComponent(motivation)}`; return request(url);}
export async function getAnnotations({ source, motivation, limit = 50, offset = 0,} = {}) { let url = `${API_BASE}/annotations?limit=${limit}&offset=${offset}`; if (source) url += `&source=${encodeURIComponent(source)}`; if (motivation) url += `&motivation=${motivation}`; return request(url);}
export async function getByTarget(source, limit = 50, offset = 0) { return request( `${API_BASE}/targets?source=${encodeURIComponent(source)}&limit=${limit}&offset=${offset}`, );}
export async function getAnnotation(uri) { return request(`${API_BASE}/annotation?uri=${encodeURIComponent(uri)}`);}
export async function getProfile(did) { return request(`${API_BASE}/profile/${encodeURIComponent(did)}`);}
export async function getUserAnnotations(did, limit = 50, offset = 0) { return request( `${API_BASE}/users/${encodeURIComponent(did)}/annotations?limit=${limit}&offset=${offset}`, );}
export async function getUserHighlights(did, limit = 50, offset = 0) { return request( `${API_BASE}/users/${encodeURIComponent(did)}/highlights?limit=${limit}&offset=${offset}`, );}
export async function getUserBookmarks(did, limit = 50, offset = 0) { return request( `${API_BASE}/users/${encodeURIComponent(did)}/bookmarks?limit=${limit}&offset=${offset}`, );}
export async function getUserTargetItems(did, url, limit = 50, offset = 0) { return request( `${API_BASE}/users/${encodeURIComponent(did)}/targets?source=${encodeURIComponent(url)}&limit=${limit}&offset=${offset}`, );}
export async function getHighlights(creatorDid, limit = 50, offset = 0) { return request( `${API_BASE}/highlights?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, );}
export async function getBookmarks(creatorDid, limit = 50, offset = 0) { return request( `${API_BASE}/bookmarks?creator=${encodeURIComponent(creatorDid)}&limit=${limit}&offset=${offset}`, );}
export async function getReplies(annotationUri) { return request( `${API_BASE}/replies?uri=${encodeURIComponent(annotationUri)}`, );}
export async function updateAnnotation(uri, text, tags) { return request(`${API_BASE}/annotations?uri=${encodeURIComponent(uri)}`, { method: "PUT", body: JSON.stringify({ text, tags }), });}
export async function updateHighlight(uri, color, tags) { return request(`${API_BASE}/highlights?uri=${encodeURIComponent(uri)}`, { method: "PUT", body: JSON.stringify({ color, tags }), });}
export async function createBookmark(url, title, description) { return request(`${API_BASE}/bookmarks`, { method: "POST", body: JSON.stringify({ url, title, description }), });}
export async function updateBookmark(uri, title, description, tags) { return request(`${API_BASE}/bookmarks?uri=${encodeURIComponent(uri)}`, { method: "PUT", body: JSON.stringify({ title, description, tags }), });}
export async function getCollections(did) { let url = `${API_BASE}/collections`; if (did) url += `?author=${encodeURIComponent(did)}`; return request(url);}
export async function getCollection(uri) { return request(`${API_BASE}/collection?uri=${encodeURIComponent(uri)}`);}
export async function getCollectionsContaining(annotationUri) { return request( `${API_BASE}/collections/containing?uri=${encodeURIComponent(annotationUri)}`, );}
export async function getEditHistory(uri) { return request( `${API_BASE}/annotations/history?uri=${encodeURIComponent(uri)}`, );}
export async function getNotifications(limit = 50, offset = 0) { return request(`${API_BASE}/notifications?limit=${limit}&offset=${offset}`);}
export async function getUnreadNotificationCount() { return request(`${API_BASE}/notifications/count`);}
export async function markNotificationsRead() { return request(`${API_BASE}/notifications/read`, { method: "POST" });}
export async function updateCollection(uri, name, description, icon) { return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { method: "PUT", body: JSON.stringify({ name, description, icon }), });}
export async function updateProfile({ bio, website, links }) { return request(`${API_BASE}/profile`, { method: "PUT", body: JSON.stringify({ bio, website, links }), });}
export async function createCollection(name, description, icon) { return request(`${API_BASE}/collections`, { method: "POST", body: JSON.stringify({ name, description, icon }), });}
export async function deleteCollection(uri) { return request(`${API_BASE}/collections?uri=${encodeURIComponent(uri)}`, { method: "DELETE", });}
export async function getCollectionItems(collectionUri) { return request( `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, );}
export async function addItemToCollection( collectionUri, annotationUri, position = 0,) { return request( `${API_BASE}/collections/${encodeURIComponent(collectionUri)}/items`, { method: "POST", body: JSON.stringify({ annotationUri, position }), }, );}
export async function removeItemFromCollection(itemUri) { return request( `${API_BASE}/collections/items?uri=${encodeURIComponent(itemUri)}`, { method: "DELETE", }, );}
export async function getLikeCount(annotationUri) { return request(`${API_BASE}/likes?uri=${encodeURIComponent(annotationUri)}`);}
export async function deleteHighlight(rkey) { return request(`${API_BASE}/highlights?rkey=${encodeURIComponent(rkey)}`, { method: "DELETE", });}
export async function deleteBookmark(rkey) { return request(`${API_BASE}/bookmarks?rkey=${encodeURIComponent(rkey)}`, { method: "DELETE", });}
export async function createHighlight({ url, title, selector, color, tags }) { return request(`${API_BASE}/highlights`, { method: "POST", body: JSON.stringify({ url, title, selector, color, tags }), });}
export async function createAnnotation({ url, text, quote, title, selector, tags,}) { return request(`${API_BASE}/annotations`, { method: "POST", body: JSON.stringify({ url, text, quote, title, selector, tags }), });}
export async function deleteAnnotation(rkey, type = "annotation") { return request( `${API_BASE}/annotations?rkey=${encodeURIComponent(rkey)}&type=${encodeURIComponent(type)}`, { method: "DELETE", }, );}
export async function likeAnnotation(subjectUri, subjectCid) { return request(`${API_BASE}/annotations/like`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ subjectUri, subjectCid, }), });}
export async function unlikeAnnotation(subjectUri) { return request( `${API_BASE}/annotations/like?uri=${encodeURIComponent(subjectUri)}`, { method: "DELETE", }, );}
export async function createReply({ parentUri, parentCid, rootUri, rootCid, text,}) { return request(`${API_BASE}/annotations/reply`, { method: "POST", body: JSON.stringify({ parentUri, parentCid, rootUri, rootCid, text }), });}
export async function deleteReply(uri) { return request( `${API_BASE}/annotations/reply?uri=${encodeURIComponent(uri)}`, { method: "DELETE", }, );}
export async function getSession() { return request(`${AUTH_BASE}/session`);}
export async function logout() { return request(`${AUTH_BASE}/logout`, { method: "POST" });}
export function normalizeAnnotation(item) { if (!item) return {};
if (item.type === "Annotation") { return { type: item.type, uri: item.uri || item.id, author: item.author || item.creator, url: item.url || item.target?.source, title: item.title || item.target?.title, text: item.text || item.body?.value, selector: item.selector || item.target?.selector, motivation: item.motivation, tags: item.tags || [], createdAt: item.createdAt || item.created, cid: item.cid || item.CID, likeCount: item.likeCount || 0, replyCount: item.replyCount || 0, viewerHasLiked: item.viewerHasLiked || false, }; }
if (item.type === "Bookmark") { return { type: item.type, uri: item.uri || item.id, author: item.author || item.creator, url: item.url || item.source, title: item.title, description: item.description, tags: item.tags || [], createdAt: item.createdAt || item.created, cid: item.cid || item.CID, likeCount: item.likeCount || 0, replyCount: item.replyCount || 0, viewerHasLiked: item.viewerHasLiked || false, }; }
if (item.type === "Highlight") { return { type: item.type, uri: item.uri || item.id, author: item.author || item.creator, url: item.url || item.target?.source, title: item.title || item.target?.title, selector: item.selector || item.target?.selector, color: item.color, tags: item.tags || [], createdAt: item.createdAt || item.created, cid: item.cid || item.CID, likeCount: item.likeCount || 0, replyCount: item.replyCount || 0, viewerHasLiked: item.viewerHasLiked || false, }; }
return { uri: item.uri || item.id, author: item.author || item.creator, url: item.url || item.source || item.target?.source, title: item.title || item.target?.title, text: item.text || item.body?.value, description: item.description, selector: item.selector || item.target?.selector, color: item.color, tags: item.tags || [], createdAt: item.createdAt || item.created, cid: item.cid || item.CID, likeCount: item.likeCount || 0, replyCount: item.replyCount || 0, viewerHasLiked: item.viewerHasLiked || false, };}
export function normalizeHighlight(highlight) { return { uri: highlight.uri || highlight.id, author: highlight.author || highlight.creator, url: highlight.url || highlight.target?.source, title: highlight.title || highlight.target?.title, selector: highlight.selector || highlight.target?.selector, color: highlight.color, tags: highlight.tags || [], createdAt: highlight.createdAt || highlight.created, likeCount: highlight.likeCount || 0, replyCount: highlight.replyCount || 0, viewerHasLiked: highlight.viewerHasLiked || false, };}
export function normalizeBookmark(bookmark) { return { uri: bookmark.uri || bookmark.id, author: bookmark.author || bookmark.creator, url: bookmark.url || bookmark.source, title: bookmark.title, description: bookmark.description, tags: bookmark.tags || [], createdAt: bookmark.createdAt || bookmark.created, likeCount: bookmark.likeCount || 0, replyCount: bookmark.replyCount || 0, viewerHasLiked: bookmark.viewerHasLiked || false, };}
export async function searchActors(query) { const res = await fetch( `https://public.api.bsky.app/xrpc/app.bsky.actor.searchActorsTypeahead?q=${encodeURIComponent(query)}&limit=5`, ); if (!res.ok) throw new Error("Search failed"); return res.json();}
export async function resolveHandle(handle) { const res = await fetch( `https://public.api.bsky.app/xrpc/com.atproto.identity.resolveHandle?handle=${encodeURIComponent(handle)}`, ); if (!res.ok) throw new Error("Failed to resolve handle"); const data = await res.json(); return data.did;}
export async function startLogin(handle, inviteCode) { return request(`${AUTH_BASE}/start`, { method: "POST", body: JSON.stringify({ handle, invite_code: inviteCode }), });}
export async function startSignup(pdsUrl) { return request(`${AUTH_BASE}/signup`, { method: "POST", body: JSON.stringify({ pds_url: pdsUrl }), });}export async function getTrendingTags(limit = 10) { return request(`${API_BASE}/tags/trending?limit=${limit}`);}
export async function getAPIKeys() { return request(`${API_BASE}/keys`);}
export async function createAPIKey(name) { return request(`${API_BASE}/keys`, { method: "POST", body: JSON.stringify({ name }), });}
export async function deleteAPIKey(id) { return request(`${API_BASE}/keys/${id}`, { method: "DELETE" });}
export async function describeServer(service) { const res = await fetch(`${service}/xrpc/com.atproto.server.describeServer`); if (!res.ok) throw new Error("Failed to describe server"); return res.json();}
export async function createAccount( service, { handle, email, password, inviteCode },) { const res = await fetch(`${service}/xrpc/com.atproto.server.createAccount`, { method: "POST", headers: { "Content-Type": "application/json", }, body: JSON.stringify({ handle, email, password, inviteCode, }), });
const data = await res.json(); if (!res.ok) { throw new Error(data.message || data.error || "Failed to create account"); } return data;}