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
90 changes: 61 additions & 29 deletions src/background/service-worker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -183,15 +183,22 @@ chrome.runtime.onMessage.addListener((message: ExtensionMessage, sender, sendRes
* Handle screenshot capture from message
*/
async function handleScreenshotCaptureMessage(message: CaptureScreenshotMessage) {
const { mode, options = {}, fromPopup } = message.payload;
return await handleScreenshotCapture(mode, { ...options, fromPopup });
const { mode, options = {}, fromPopup, target } = message.payload;
return await handleScreenshotCapture(mode, { ...options, fromPopup, target });
}

// Store pending selection resolve/reject callbacks
type PendingSelectionTarget = {
tabId: number;
windowId: number;
url?: string;
};

let pendingSelectionResolve: ((value: any) => void) | null = null;
let pendingSelectionReject: ((reason: any) => void) | null = null;
let pendingSelectionFromPopup = false;
let pendingSelectionTimeoutId: ReturnType<typeof setTimeout> | null = null;
let pendingSelectionTarget: PendingSelectionTarget | null = null;

function clearPendingSelectionTimeout(): void {
if (pendingSelectionTimeoutId !== null) {
Expand All @@ -200,22 +207,28 @@ function clearPendingSelectionTimeout(): void {
}
}

function resetPendingSelectionState(): void {
pendingSelectionResolve = null;
pendingSelectionReject = null;
pendingSelectionFromPopup = false;
pendingSelectionTarget = null;
}

function rejectPendingSelection(error: unknown): void {
if (pendingSelectionReject) {
clearPendingSelectionTimeout();
pendingSelectionReject(error);
pendingSelectionResolve = null;
pendingSelectionReject = null;
pendingSelectionFromPopup = false;
const reject = pendingSelectionReject;
clearPendingSelectionTimeout();
if (reject) {
reject(error);
}
resetPendingSelectionState();
}

/**
* Handle selection mode capture
* Injects content script and waits for user selection
*/
async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise<any> {
if (!tab.id) {
async function handleSelectionCapture(tab: chrome.tabs.Tab, fromPopup: boolean): Promise<any> {
if (!tab.id || !tab.windowId) {
throw new Error('No active tab found');
}

Expand All @@ -229,6 +242,8 @@ async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise<any> {
rejectPendingSelection(new Error('Selection cancelled: a new selection was started'));
}

pendingSelectionFromPopup = fromPopup;

// Inject the selection overlay content script
try {
await chrome.scripting.executeScript({
Expand All @@ -240,10 +255,17 @@ async function handleSelectionCapture(tab: chrome.tabs.Tab): Promise<any> {
throw new Error('Failed to start selection mode. Make sure you are on a valid web page.');
}

const selectionTarget: PendingSelectionTarget = {
tabId: tab.id,
windowId: tab.windowId,
url: tab.url,
};

// Wait for selection to complete via message
return new Promise((resolve, reject) => {
pendingSelectionResolve = resolve;
pendingSelectionReject = reject;
pendingSelectionTarget = selectionTarget;

// Timeout after 60 seconds
pendingSelectionTimeoutId = setTimeout(() => {
Expand Down Expand Up @@ -293,12 +315,11 @@ async function handleSelectionComplete(payload: unknown) {
const reason = typeof p.reason === 'string' ? p.reason : undefined;
logger.log('Selection cancelled:', reason);
clearPendingSelectionTimeout();
if (pendingSelectionResolve) {
pendingSelectionResolve({ cancelled: true, reason });
pendingSelectionResolve = null;
pendingSelectionReject = null;
pendingSelectionFromPopup = false;
const resolve = pendingSelectionResolve;
if (resolve) {
resolve({ cancelled: true, reason });
}
resetPendingSelectionState();
return;
}

Expand All @@ -309,8 +330,10 @@ async function handleSelectionComplete(payload: unknown) {
logger.log('Selection complete:', coordinates);

try {
// Get the active tab to capture
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
// Capture the same tab/window where the selection overlay was injected.
const tab = pendingSelectionTarget
? await chrome.tabs.get(pendingSelectionTarget.tabId)
: (await chrome.tabs.query({ active: true, lastFocusedWindow: true }))[0];
if (!tab.id || !tab.windowId) {
throw new Error('No active tab found');
}
Expand Down Expand Up @@ -408,13 +431,15 @@ async function handleSelectionComplete(payload: unknown) {

await assetStorage.setAsset(asset);

const shouldAutoUpload = settings.autoUpload && !pendingSelectionFromPopup;

// Show notification
await showCaptureNotification(settings.autoUpload && !pendingSelectionFromPopup);
await showCaptureNotification(shouldAutoUpload);
await updateExtensionBadge();

// Auto-upload if enabled and not initiated from popup
// (popup handles upload after showing headline/caption modal)
if (settings.autoUpload && !pendingSelectionFromPopup) {
if (shouldAutoUpload) {
try {
const numbersApi = await getNumbersApi();
let auth = numbersApi.auth.isAuthenticated();
Expand Down Expand Up @@ -453,17 +478,16 @@ async function handleSelectionComplete(payload: unknown) {
clearTimeout(pendingSelectionTimeoutId);
pendingSelectionTimeoutId = null;
}
if (pendingSelectionResolve) {
pendingSelectionResolve({
const resolve = pendingSelectionResolve;
if (resolve) {
resolve({
assetId,
dataUrl,
timestamp: captureTime.toISOString(),
autoUpload: settings.autoUpload && !pendingSelectionFromPopup,
autoUpload: shouldAutoUpload,
});
pendingSelectionResolve = null;
pendingSelectionReject = null;
pendingSelectionFromPopup = false;
}
resetPendingSelectionState();
} catch (error: any) {
logger.error('Failed to capture selection:', error);
rejectPendingSelection(error);
Expand Down Expand Up @@ -499,16 +523,24 @@ async function handleScreenshotCapture(
) {
const fromPopup = options?.fromPopup === true;
try {
// Get current active tab first
const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
// Prefer the tab captured by the popup click handler. In a service worker,
// currentWindow can be ambiguous once focus has moved to the extension popup.
const tab = options?.target?.tabId
? await chrome.tabs.get(options.target.tabId)
: (await chrome.tabs.query({ active: true, lastFocusedWindow: true }))[0];

if (!tab.id || !tab.windowId) {
throw new Error('No active tab found');
}

// Handle selection mode - inject content script and wait for selection
if (mode === 'selection') {
pendingSelectionFromPopup = fromPopup;
return await handleSelectionCapture(tab);
try {
return await handleSelectionCapture(tab, fromPopup);
} catch (error) {
resetPendingSelectionState();
throw error;
}
}

// Capture timestamp at the very start for consistency
Expand Down
10 changes: 10 additions & 0 deletions src/popup/popup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@
/**
* Main Popup Component
*/
function PopupApp() {

Check warning on line 28 in src/popup/popup.tsx

View workflow job for this annotation

GitHub Actions / ci

Fast refresh only works when a file has exports. Move your component(s) to a separate file
const [isAuthenticated, setIsAuthenticated] = useState(false);
const [isLoading, setIsLoading] = useState(true);
const [assets, setAssets] = useState<Asset[]>([]);
Expand All @@ -40,7 +40,7 @@

useEffect(() => {
loadInitialData();
}, []);

Check warning on line 43 in src/popup/popup.tsx

View workflow job for this annotation

GitHub Actions / ci

React Hook useEffect has a missing dependency: 'loadInitialData'. Either include it or remove the dependency array

async function loadInitialData() {
setIsLoading(true);
Expand Down Expand Up @@ -102,11 +102,21 @@
async function handleCapture(mode: 'visible' | 'selection' = captureMode) {
setCapturing(true);
try {
const [tab] = await chrome.tabs.query({ active: true, lastFocusedWindow: true });
if (!tab?.id || !tab.windowId) {
throw new Error('No active tab found');
}

const response = await chrome.runtime.sendMessage({
type: 'CAPTURE_SCREENSHOT',
payload: {
mode: mode,
fromPopup: true, // Skip auto-upload so user can add headline/caption first
target: {
tabId: tab.id,
windowId: tab.windowId,
url: tab.url,
},
},
});

Expand Down Expand Up @@ -286,7 +296,7 @@

chrome.runtime.onMessage.addListener(handleMessage);
return () => chrome.runtime.onMessage.removeListener(handleMessage);
}, [showInsufficientCreditsNotification, huntMode.enabled]); // Add huntMode dependency

Check warning on line 299 in src/popup/popup.tsx

View workflow job for this annotation

GitHub Actions / ci

React Hook useEffect has a missing dependency: 'checkCreditStatus'. Either include it or remove the dependency array

if (isLoading) {
return (
Expand Down
5 changes: 5 additions & 0 deletions src/types/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,5 +60,10 @@ export interface CaptureScreenshotMessage {
mode: CaptureMode;
options?: Partial<ScreenshotOptions>;
fromPopup?: boolean; // When true, skip auto-upload to allow adding metadata first
target?: {
tabId: number;
windowId: number;
url?: string;
};
};
}
Loading