(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.

Implement canonical annotations #15

scanash00 44a2bde7 419a4460

+76 -10
+37 -8
extension/background/service-worker.js
··· 205 205 206 206 if (info.menuItemId === "margin-annotate") { 207 207 let selector = null; 208 + let canonicalUrl = null; 208 209 209 210 try { 210 211 const response = await chrome.tabs.sendMessage(tab.id, { ··· 212 213 selectionText: info.selectionText, 213 214 }); 214 215 selector = response?.selector; 216 + canonicalUrl = response?.canonicalUrl; 215 217 } catch { 216 218 /* ignore */ 217 219 } ··· 223 225 }; 224 226 } 225 227 228 + const targetUrl = canonicalUrl || tab.url; 229 + 226 230 if (selector) { 227 231 try { 228 232 await chrome.tabs.sendMessage(tab.id, { 229 233 type: "SHOW_INLINE_ANNOTATE", 230 234 data: { 231 - url: tab.url, 235 + url: targetUrl, 232 236 title: tab.title, 233 237 selector: selector, 234 238 }, ··· 240 244 } 241 245 242 246 if (WEB_BASE) { 243 - let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(tab.url)}`; 247 + let composeUrl = `${WEB_BASE}/new?url=${encodeURIComponent(targetUrl)}`; 244 248 if (selector) { 245 249 composeUrl += `&selector=${encodeURIComponent(JSON.stringify(selector))}`; 246 250 } ··· 251 255 252 256 if (info.menuItemId === "margin-highlight") { 253 257 let selector = null; 258 + let canonicalUrl = null; 254 259 255 260 try { 256 261 const response = await chrome.tabs.sendMessage(tab.id, { ··· 259 264 }); 260 265 if (response?.selector) { 261 266 selector = response.selector; 267 + canonicalUrl = response.canonicalUrl; 262 268 } 263 269 if (response && response.success) return; 264 270 } catch { ··· 271 277 exact: info.selectionText, 272 278 }; 273 279 } 280 + 281 + const targetUrl = canonicalUrl || tab.url; 274 282 275 283 if (selector) { 276 284 try { 277 285 await createHighlight({ 278 - url: tab.url, 286 + url: targetUrl, 279 287 title: tab.title, 280 288 selector: selector, 281 289 }); ··· 359 367 : API_BASE; 360 368 361 369 const pageUrl = request.data.url; 362 - const res = await fetch( 363 - `${currentApiUrl}/api/targets?source=${encodeURIComponent(pageUrl)}`, 370 + const citedUrls = request.data.citedUrls || []; 371 + const uniqueUrls = [...new Set([pageUrl, ...citedUrls])]; 372 + 373 + const fetchPromises = uniqueUrls.map((u) => 374 + fetch( 375 + `${currentApiUrl}/api/targets?source=${encodeURIComponent(u)}`, 376 + ).then((r) => r.json().catch(() => ({}))), 364 377 ); 365 - const data = await res.json(); 366 378 367 - const items = [...(data.annotations || []), ...(data.highlights || [])]; 368 - sendResponse({ success: true, data: items }); 379 + const results = await Promise.all(fetchPromises); 380 + let allItems = []; 381 + const seenIds = new Set(); 382 + 383 + results.forEach((data) => { 384 + const items = [ 385 + ...(data.annotations || []), 386 + ...(data.highlights || []), 387 + ]; 388 + items.forEach((item) => { 389 + const id = item.uri || item.id; 390 + if (id && !seenIds.has(id)) { 391 + seenIds.add(id); 392 + allItems.push(item); 393 + } 394 + }); 395 + }); 396 + 397 + sendResponse({ success: true, data: allItems }); 369 398 370 399 if (sender.tab) { 371 400 const count = items.length;
+39 -2
extension/content/content.js
··· 1024 1024 1025 1025 function fetchAnnotations(retryCount = 0) { 1026 1026 if (typeof chrome !== "undefined" && chrome.runtime) { 1027 + const citedUrls = Array.from(document.querySelectorAll("[cite]")) 1028 + .map((el) => el.getAttribute("cite")) 1029 + .filter((url) => url && url.startsWith("http")); 1030 + const uniqueCitedUrls = [...new Set(citedUrls)]; 1031 + 1027 1032 chrome.runtime.sendMessage( 1028 1033 { 1029 1034 type: "GET_ANNOTATIONS", 1030 - data: { url: window.location.href }, 1035 + data: { 1036 + url: window.location.href, 1037 + citedUrls: uniqueCitedUrls, 1038 + }, 1031 1039 }, 1032 1040 (res) => { 1033 1041 if (res && res.success && res.data && res.data.length > 0) { ··· 1043 1051 } 1044 1052 } 1045 1053 1054 + function findCanonicalUrl(range) { 1055 + if (!range) return null; 1056 + let node = range.commonAncestorContainer; 1057 + if (node.nodeType === Node.TEXT_NODE) { 1058 + node = node.parentNode; 1059 + } 1060 + 1061 + while (node && node !== document.body) { 1062 + if ( 1063 + (node.tagName === "BLOCKQUOTE" || node.tagName === "Q") && 1064 + node.hasAttribute("cite") 1065 + ) { 1066 + if (node.contains(range.commonAncestorContainer)) { 1067 + return node.getAttribute("cite"); 1068 + } 1069 + } 1070 + node = node.parentNode; 1071 + } 1072 + return null; 1073 + } 1074 + 1046 1075 chrome.runtime.onMessage.addListener((request, sender, sendResponse) => { 1047 1076 if (request.type === "GET_SELECTOR_FOR_ANNOTATE_INLINE") { 1048 1077 const sel = window.getSelection(); ··· 1051 1080 return true; 1052 1081 } 1053 1082 const exact = sel.toString().trim(); 1054 - sendResponse({ selector: { type: "TextQuoteSelector", exact } }); 1083 + const canonicalUrl = findCanonicalUrl(sel.getRangeAt(0)); 1084 + 1085 + sendResponse({ 1086 + selector: { type: "TextQuoteSelector", exact }, 1087 + canonicalUrl, 1088 + }); 1055 1089 return true; 1056 1090 } 1057 1091 ··· 1074 1108 return true; 1075 1109 } 1076 1110 const exact = sel.toString().trim(); 1111 + const canonicalUrl = findCanonicalUrl(sel.getRangeAt(0)); 1112 + 1077 1113 sendResponse({ 1078 1114 success: false, 1079 1115 selector: { type: "TextQuoteSelector", exact }, 1116 + canonicalUrl, 1080 1117 }); 1081 1118 return true; 1082 1119 }