();
foundItems.forEach(({ item }) => {
const author = item.author || item.creator || {};
const id = author.did || author.handle || 'unknown';
if (!authorsMap.has(id)) {
authorsMap.set(id, author);
}
});
const uniqueAuthors = Array.from(authorsMap.values());
const maxShow = 3;
const displayAuthors = uniqueAuthors.slice(0, maxShow);
const overflow = uniqueAuthors.length - maxShow;
let html = displayAuthors
.map((author, i) => {
const avatar = author.avatar;
const handle = author.handle || 'U';
const marginLeft = i === 0 ? '0' : '-8px';
if (avatar) {
return `
`;
} else {
return `${handle[0]?.toUpperCase() || 'U'}
`;
}
})
.join('');
if (overflow > 0) {
html += `+${overflow}
`;
}
hoverIndicator.innerHTML = html;
const firstRect = firstRange.getClientRects()[0];
const totalWidth =
Math.min(uniqueAuthors.length, maxShow + (overflow > 0 ? 1 : 0)) * 18 + 8;
const leftPos = firstRect.left - totalWidth;
const topPos = firstRect.top + firstRect.height / 2 - 12;
hoverIndicator.style.left = `${leftPos}px`;
hoverIndicator.style.top = `${topPos}px`;
hoverIndicator.classList.add('visible');
}
} else {
document.body.style.cursor = '';
if (hoverIndicator) {
hoverIndicator.classList.remove('visible');
}
}
}
function handleDocumentClick(e: MouseEvent) {
if (!overlayEnabled || !overlayHost) return;
const x = e.clientX;
const y = e.clientY;
if (popoverEl) {
const rect = popoverEl.getBoundingClientRect();
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
return;
}
}
if (composeModal) {
const rect = composeModal.getBoundingClientRect();
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
return;
}
composeModal.remove();
composeModal = null;
}
const clickedItems: Annotation[] = [];
for (const { range, item } of activeItems) {
const rects = range.getClientRects();
for (const rect of rects) {
if (x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom) {
let container: Node | null = range.commonAncestorContainer;
if (container.nodeType === Node.TEXT_NODE) {
container = container.parentNode;
}
if (
container &&
((e.target as Node).contains(container) || container.contains(e.target as Node))
) {
if (!clickedItems.includes(item)) {
clickedItems.push(item);
}
}
break;
}
}
}
if (clickedItems.length > 0) {
e.preventDefault();
e.stopPropagation();
if (popoverEl) {
const currentIds = popoverEl.dataset.itemIds;
const newIds = clickedItems
.map((i) => i.uri || i.id)
.sort()
.join(',');
if (currentIds === newIds) {
popoverEl.remove();
popoverEl = null;
return;
}
}
const firstItem = clickedItems[0];
const match = activeItems.find((x) => x.item === firstItem);
if (match) {
const rects = match.range.getClientRects();
if (rects.length > 0) {
const rect = rects[0];
const top = rect.top + window.scrollY;
const left = rect.left + window.scrollX;
showPopover(clickedItems, top, left);
}
}
} else {
if (popoverEl) {
popoverEl.remove();
popoverEl = null;
}
}
}
function showPopover(items: Annotation[], top: number, left: number) {
if (!shadowRoot) return;
if (popoverEl) popoverEl.remove();
const container = shadowRoot.getElementById('margin-overlay-container');
if (!container) return;
popoverEl = document.createElement('div');
popoverEl.className = 'margin-popover';
const ids = items
.map((i) => i.uri || i.id)
.sort()
.join(',');
popoverEl.dataset.itemIds = ids;
const popWidth = 320;
const screenWidth = window.innerWidth;
let finalLeft = left;
if (left + popWidth > screenWidth) finalLeft = screenWidth - popWidth - 20;
if (finalLeft < 10) finalLeft = 10;
popoverEl.style.top = `${top + 24}px`;
popoverEl.style.left = `${finalLeft}px`;
const count = items.length;
const title = count === 1 ? 'Annotation' : `Annotations`;
const contentHtml = items
.map((item) => {
const author = item.author || item.creator || {};
const handle = author.handle || 'User';
const avatar = author.avatar;
const text = item.body?.value || item.text || '';
const id = item.id || item.uri;
const isHighlight = (item as any).type === 'Highlight';
const isOwned = currentUserDid && author.did === currentUserDid;
const createdAt = item.createdAt ? formatRelativeTime(item.createdAt) : '';
let avatarHtml = ``;
if (avatar) {
avatarHtml = ``;
}
let bodyHtml = '';
if (isHighlight && !text) {
bodyHtml = `${Icons.highlightMarker} Highlighted
`;
} else {
bodyHtml = ``;
}
const addNoteBtn =
isHighlight && isOwned
? ``
: '';
return `
`;
})
.join('');
popoverEl.innerHTML = `
${contentHtml}
`;
popoverEl.querySelector('.popover-close')?.addEventListener('click', (e) => {
e.stopPropagation();
popoverEl?.remove();
popoverEl = null;
});
popoverEl.querySelectorAll('.btn-add-note').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const uri = (btn as HTMLElement).getAttribute('data-uri') || '';
const itemId = (btn as HTMLElement).getAttribute('data-id') || '';
const commentItem = btn.closest('.comment-item');
if (!commentItem) return;
if (commentItem.querySelector('.add-note-form')) return;
const form = document.createElement('div');
form.className = 'add-note-form';
form.innerHTML = `
`;
commentItem.appendChild(form);
const textarea = form.querySelector('textarea') as HTMLTextAreaElement;
textarea?.focus();
textarea?.addEventListener('keydown', (ke) => {
if (ke.key === 'Enter' && !ke.shiftKey) {
ke.preventDefault();
submitNote();
}
if (ke.key === 'Escape') {
form.remove();
}
});
form.querySelector('.add-note-cancel')?.addEventListener('click', (ce) => {
ce.stopPropagation();
form.remove();
});
form.querySelector('.add-note-submit')?.addEventListener('click', (se) => {
se.stopPropagation();
submitNote();
});
async function submitNote() {
const noteText = textarea?.value.trim();
if (!noteText) return;
const submitBtn = form.querySelector('.add-note-submit') as HTMLButtonElement;
if (submitBtn) submitBtn.disabled = true;
textarea.disabled = true;
try {
const matchingItem = items.find((i) => (i.id || i.uri) === itemId);
const selector = matchingItem?.target?.selector || matchingItem?.selector;
const result = await sendMessage('convertHighlightToAnnotation', {
highlightUri: uri,
url: getPageUrl(),
title: document.title,
text: noteText,
selector: selector ? { type: 'TextQuoteSelector', exact: selector.exact } : undefined,
});
if (result.success) {
showToast('Highlight converted to annotation!', 'success');
popoverEl?.remove();
popoverEl = null;
cachedMatcher = null;
setTimeout(() => fetchAnnotations(), 500);
} else {
showToast('Failed to convert', 'error');
if (submitBtn) submitBtn.disabled = false;
textarea.disabled = false;
}
} catch {
showToast('Failed to convert', 'error');
if (submitBtn) submitBtn.disabled = false;
textarea.disabled = false;
}
}
});
});
popoverEl.querySelectorAll('.btn-reply').forEach((btn) => {
btn.addEventListener('click', (e) => {
e.stopPropagation();
const id = (btn as HTMLElement).getAttribute('data-id');
if (id) {
window.open(`${APP_URL}/annotation/${encodeURIComponent(id)}`, '_blank');
}
});
});
popoverEl.querySelectorAll('.btn-share').forEach((btn) => {
btn.addEventListener('click', async (_e) => {
const text = (btn as HTMLElement).getAttribute('data-text') || '';
try {
await navigator.clipboard.writeText(text);
const originalInner = btn.innerHTML;
btn.innerHTML = `${Icons.check} Copied!`;
setTimeout(() => {
btn.innerHTML = originalInner;
}, 2000);
} catch (error) {
console.error('Failed to copy', error);
}
});
});
container.appendChild(popoverEl);
}
let lastPolledUrl = getPageUrl();
function onUrlChange() {
lastPolledUrl = getPageUrl();
if (typeof CSS !== 'undefined' && CSS.highlights) {
CSS.highlights.clear();
}
injectedStyles.clear();
document.querySelectorAll('style').forEach((s) => {
if (s.textContent?.includes('::highlight(margin-hl-')) s.remove();
});
activeItems = [];
cachedMatcher = null;
sendMessage('updateBadge', { count: 0 });
if (overlayEnabled) {
setTimeout(() => fetchAnnotations(), 300);
}
}
window.addEventListener('popstate', onUrlChange);
const originalPushState = history.pushState;
const originalReplaceState = history.replaceState;
history.pushState = function (...args) {
originalPushState.apply(this, args);
onUrlChange();
};
history.replaceState = function (...args) {
originalReplaceState.apply(this, args);
onUrlChange();
};
// Only re-fetch when the URL actually changes (not every 500ms)
setInterval(() => {
const currentUrl = getPageUrl();
if (currentUrl !== lastPolledUrl) {
onUrlChange();
}
}, 500);
let domChangeTimeout: ReturnType | null = null;
let domChangeCount = 0;
const observer = new MutationObserver((mutations) => {
const hasSignificantChange = mutations.some(
(m) => m.type === 'childList' && (m.addedNodes.length > 3 || m.removedNodes.length > 3)
);
if (hasSignificantChange && overlayEnabled) {
domChangeCount++;
if (domChangeTimeout) clearTimeout(domChangeTimeout);
const delay = Math.min(500 + domChangeCount * 100, 2000);
domChangeTimeout = setTimeout(() => {
cachedMatcher = null;
domChangeCount = 0;
fetchAnnotations();
}, delay);
}
});
observer.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
if (document.querySelector('.pdfViewer') || /\.pdf(\?|#|$)/i.test(window.location.href)) {
const pdfObserver = new MutationObserver(() => {
const textLayers = document.querySelectorAll('.textLayer span');
if (textLayers.length > 10) {
if (domChangeTimeout) clearTimeout(domChangeTimeout);
domChangeTimeout = setTimeout(() => {
cachedMatcher = null;
fetchAnnotations();
}, 1000);
}
});
pdfObserver.observe(document.body || document.documentElement, {
childList: true,
subtree: true,
});
ctx.onInvalidated(() => {
pdfObserver.disconnect();
});
}
ctx.onInvalidated(() => {
observer.disconnect();
});
window.addEventListener('load', () => {
setTimeout(() => fetchAnnotations(), 500);
});
}