Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 40 additions & 20 deletions entrypoints/background.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,53 @@ import {
isExtensionSender,
getContentScriptFiles,
} from '@/utils/background-helpers';
import {pendingCaptureTabs} from '@/utils/storage-items';
import {backgroundMessenger, contentMessenger} from '@/utils/messaging';

export default defineBackground(() => {
const pendingCaptureTabs = new Map<number, number>();
const pendingCaptureTtlMs = 120000;
const contextMenuId = 'df-export-snapshot';

function markPendingCapture(tabId: number): void {
pendingCaptureTabs.set(tabId, Date.now());
function trimExpiredPending(
current: Record<string, number>
): Record<string, number> {
const now = Date.now();
let changed = false;
const next: Record<string, number> = {...current};
for (const [key, startedAt] of Object.entries(current)) {
if (!startedAt || now - startedAt > pendingCaptureTtlMs) {
delete next[key];
changed = true;
}
}
return changed ? next : current;
}

function hasPendingCapture(tabId: number): boolean {
const startedAt = pendingCaptureTabs.get(tabId);
if (!startedAt) return false;
if (Date.now() - startedAt > pendingCaptureTtlMs) {
pendingCaptureTabs.delete(tabId);
return false;
async function markPendingCapture(tabId: number): Promise<void> {
const key = String(tabId);
const current = await pendingCaptureTabs.getValue();
const trimmed = trimExpiredPending(current);
await pendingCaptureTabs.setValue({...trimmed, [key]: Date.now()});
}

async function hasPendingCapture(tabId: number): Promise<boolean> {
const key = String(tabId);
const current = await pendingCaptureTabs.getValue();
if (!current[key]) return false;
const trimmed = trimExpiredPending(current);
if (trimmed !== current) {
await pendingCaptureTabs.setValue(trimmed);
}
return true;
return Boolean(trimmed[key]);
}

function clearPendingCapture(tabId: number): void {
pendingCaptureTabs.delete(tabId);
async function clearPendingCapture(tabId: number): Promise<void> {
const key = String(tabId);
const current = await pendingCaptureTabs.getValue();
if (!current[key]) return;
const next = {...current};
delete next[key];
await pendingCaptureTabs.setValue(next);
}

async function injectContentScripts(tabId: number): Promise<boolean> {
Expand Down Expand Up @@ -122,10 +146,10 @@ export default defineBackground(() => {

await showToolbar(tab.id);

if (hasPendingCapture(tab.id)) {
if (await hasPendingCapture(tab.id)) {
const resumed = await sendResumeExportWithRetry(tab.id);
if (resumed) {
clearPendingCapture(tab.id);
await clearPendingCapture(tab.id);
}
}
}
Expand All @@ -143,10 +167,6 @@ export default defineBackground(() => {
await handleUserInvocation(activeTab);
});

browser.tabs.onRemoved.addListener(tabId => {
clearPendingCapture(tabId);
});

backgroundMessenger.onMessage('captureScreenshot', async ({sender}) => {
if (!isExtensionSender(sender)) {
throw new Error('Invalid sender');
Expand All @@ -156,11 +176,11 @@ export default defineBackground(() => {
const result = await captureVisibleTabScreenshot(windowId);
if (result.error && sender.tab?.id) {
if (isActiveTabPermissionError(result.error)) {
markPendingCapture(sender.tab.id);
await markPendingCapture(sender.tab.id);
return {...result, errorCode: 'activeTab-required' as const};
}
} else if (sender.tab?.id) {
clearPendingCapture(sender.tab.id);
await clearPendingCapture(sender.tab.id);
}
return result;
} catch (error) {
Expand Down
1 change: 1 addition & 0 deletions entrypoints/content/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ export default defineContentScript({
registration: 'runtime',
world: 'ISOLATED',
cssInjectionMode: 'ui',
allFrames: false,

main(ctx) {
const isEligibleDocument =
Expand Down
7 changes: 5 additions & 2 deletions entrypoints/offscreen/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ async function handleDownload(
}
}

browser.runtime.onMessage.addListener((message, sender) => {
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (sender.id && sender.id !== browser.runtime.id) {
return;
}
Expand All @@ -68,5 +68,8 @@ browser.runtime.onMessage.addListener((message, sender) => {
) {
return;
}
return handleDownload(message as OffscreenDownloadMessage);
handleDownload(message as OffscreenDownloadMessage)
.then(response => sendResponse(response))
.catch(error => sendResponse({ok: false, error: String(error)}));
return true;
});
11 changes: 10 additions & 1 deletion entrypoints/test-activate/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ interface TargetInfo {
targetUrl: string;
}

const isE2E = import.meta.env.VITE_DF_E2E === '1';

function getTargetInfo(params: URLSearchParams): TargetInfo {
const target = params.get('target') ?? '';
const targetUrl = target.trim();
Expand Down Expand Up @@ -112,4 +114,11 @@ async function activate(targetInfo: TargetInfo): Promise<void> {

const params = new URLSearchParams(window.location.search);
const targetInfo = getTargetInfo(params);
void activate(targetInfo);
if (!isE2E) {
window.__dfActivateStatus = 'disabled';
window.__dfActivateDebug = {
reason: 'test-activate is only available in E2E builds',
};
} else {
void activate(targetInfo);
}
1 change: 1 addition & 0 deletions tests/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export interface DfActivateDebug {
flagCheck?: unknown;
toolbarShown?: boolean;
error?: string;
reason?: string;
}

declare global {
Expand Down
8 changes: 8 additions & 0 deletions utils/storage-items.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,3 +25,11 @@ export const toolbarPositions = storage.defineItem<
fallback: {},
version: 1,
});

export const pendingCaptureTabs = storage.defineItem<Record<string, number>>(
'session:designer-feedback:pending-capture-tabs',
{
fallback: {},
version: 1,
}
);
21 changes: 20 additions & 1 deletion utils/storage.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ import {
import {hashString} from '@/utils/hash';
import {storage} from 'wxt/utils/storage';
import {backgroundMessenger} from '@/utils/messaging';
import {maybeRunCleanup, getRetentionCutoff} from '@/utils/storage-cleanup';
import {
maybeRunCleanup,
getRetentionCutoff,
cleanupExpiredAnnotations,
} from '@/utils/storage-cleanup';
import {getWindow} from '@/utils/dom/guards';

export function getStorageKey(targetUrl?: string): string {
Expand Down Expand Up @@ -62,6 +66,12 @@ async function getLocal<T>(key: string, fallback: T): Promise<T> {
}
}

function isQuotaError(error: unknown): boolean {
if (!error) return false;
const message = error instanceof Error ? error.message : String(error);
return message.toLowerCase().includes('quota');
}

async function setLocal(values: Record<string, unknown>): Promise<void> {
const entries = Object.entries(values).map(([key, value]) => ({
key: `local:${key}` as const,
Expand All @@ -71,6 +81,15 @@ async function setLocal(values: Record<string, unknown>): Promise<void> {
try {
await storage.setItems(entries);
} catch (error) {
if (isQuotaError(error)) {
try {
await cleanupExpiredAnnotations(getRetentionCutoff());
await storage.setItems(entries);
return;
} catch (retryError) {
console.warn('Storage quota exceeded; retry failed:', retryError);
}
}
console.warn('Storage access failed (set):', error);
}
}
Expand Down
66 changes: 39 additions & 27 deletions wxt.config.ts
Original file line number Diff line number Diff line change
@@ -1,45 +1,57 @@
import {defineConfig} from 'wxt';

const isE2E = process.env.VITE_DF_E2E === '1';

export default defineConfig({
modules: ['@wxt-dev/module-react', '@wxt-dev/auto-icons'],
srcDir: '.',
outDir: '.output',

manifest: {
name: 'Designer Feedback',
description:
'Annotate any webpage and share visual feedback with developers',
permissions: [
manifest: ({browser}) => {
const permissions = [
'storage',
'tabs',
'downloads',
'offscreen',
'activeTab',
'scripting',
'contextMenus',
],
commands: {
'activate-toolbar': {
suggested_key: {
default: 'Ctrl+Shift+S',
mac: 'Command+Shift+S',
];

if (browser !== 'firefox') {
permissions.push('offscreen');
}

if (isE2E) {
permissions.push('tabs');
}

return {
name: 'Designer Feedback',
description:
'Annotate any webpage and share visual feedback with developers',
permissions,
commands: {
'activate-toolbar': {
suggested_key: {
default: 'Ctrl+Shift+S',
mac: 'Command+Shift+S',
},
description: 'Open Designer Feedback on the current page',
},
description: 'Open Designer Feedback on the current page',
},
},
content_security_policy: {
extension_pages: "script-src 'self'; object-src 'none';",
},
action: {
default_title: 'Click to activate Designer Feedback',
},
// CSS must be accessible from all URLs for runtime content script injection
web_accessible_resources: [
{
resources: ['content-scripts/content.css'],
matches: ['http://*/*', 'https://*/*'],
content_security_policy: {
extension_pages: "script-src 'self'; object-src 'none';",
},
action: {
default_title: 'Click to activate Designer Feedback',
},
],
// CSS must be accessible from all URLs for runtime content script injection
web_accessible_resources: [
{
resources: ['content-scripts/content.css'],
matches: ['http://*/*', 'https://*/*'],
},
],
};
},

// Maintain old output directory structure for E2E tests
Expand Down
Loading