Skip to content
Closed
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
41 changes: 41 additions & 0 deletions public/background.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,44 @@ chrome.runtime.onInstalled.addListener(function (object) {

// Shows an uninstall survey when extension is removed
chrome.runtime.setUninstallURL("https://tally.so/r/w4yg5X");



// adding archiveAutomation content script
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'archiveEmails') {
handleArchiveEmails(message.data)
.then(result => sendResponse({ success: true, result }))
.catch(error => sendResponse({ success: false, error: error.message }));
return true; // Required for async response
}
// ... existing handlers
});

async function handleArchiveEmails(data: { sender: string }) {
const { sender } = data;

try {
// Query the active Gmail tab
const tabs = await chrome.tabs.query({
active: true,
currentWindow: true,
url: "*://mail.google.com/*"
});

if (!tabs[0]?.id) {
throw new Error('No active Gmail tab found');
}

// Execute archive operation in content script
const result = await chrome.tabs.sendMessage(tabs[0].id, {
action: 'performArchive',
sender: sender
});

return result;
} catch (error) {
console.error('Archive error:', error);
throw error;
}
}
164 changes: 164 additions & 0 deletions src/data/content_scripts/archiveAutomation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
export class ArchiveAutomation {
private readonly SELECTORS = {
searchBox: 'input[name="q"]',
searchButton: 'button[aria-label="Search Mail"]',
selectAllCheckbox: 'div[data-tooltip="Select"]',
archiveButton: 'div[data-tooltip="Archive"]',
emailRow: 'tr.zA',
loadingIndicator: 'div[role="progressbar"]'
};

async archiveEmailsFromSender(senderEmail: string): Promise {
try {
// Navigate to All Mail view to ensure we see all emails
await this.navigateToAllMail();

// Search for emails from sender
await this.searchBySender(senderEmail);

// Wait for results to load
await this.waitForSearchResults();

// Count emails found
const emailCount = this.countEmailsInView();

if (emailCount === 0) {
return {
success: true,
archivedCount: 0,
error: 'No emails found from this sender'
};
}

// Select all emails
await this.selectAllEmails();

// Click archive button
await this.clickArchiveButton();

// Wait for archive to complete
await this.waitForArchiveComplete();

return {
success: true,
archivedCount: emailCount
};
} catch (error) {
console.error('Archive automation error:', error);
return {
success: false,
archivedCount: 0,
error: error instanceof Error ? error.message : 'Unknown error'
};
}
}

private async navigateToAllMail(): Promise {
const allMailLink = document.querySelector('a[href="#all"]');
if (allMailLink instanceof HTMLElement) {
allMailLink.click();
await this.wait(1000);
}
}

private async searchBySender(senderEmail: string): Promise {
const searchBox = document.querySelector(this.SELECTORS.searchBox) as HTMLInputElement;
if (!searchBox) {
throw new Error('Search box not found');
}

// Clear existing search
searchBox.value = '';
searchBox.focus();

// Type search query
const searchQuery = `from:${senderEmail}`;
searchBox.value = searchQuery;

// Trigger search
const event = new KeyboardEvent('keydown', {
key: 'Enter',
code: 'Enter',
keyCode: 13,
bubbles: true
});
searchBox.dispatchEvent(event);

await this.wait(1500);
}

private async waitForSearchResults(): Promise {
// Wait for loading indicator to disappear
let attempts = 0;
const maxAttempts = 20;

while (attempts < maxAttempts) {
const loading = document.querySelector(this.SELECTORS.loadingIndicator);
if (!loading) {
// Wait a bit more for results to stabilize
await this.wait(500);
return;
}
await this.wait(500);
attempts++;
}
}

private countEmailsInView(): number {
const emailRows = document.querySelectorAll(this.SELECTORS.emailRow);
return emailRows.length;
}

private async selectAllEmails(): Promise {
// Click the select all checkbox
const selectAllBtn = document.querySelector(this.SELECTORS.selectAllCheckbox) as HTMLElement;
if (!selectAllBtn) {
throw new Error('Select all button not found');
}

selectAllBtn.click();
await this.wait(300);

// Check if "Select all conversations" banner appears
const selectAllConversationsLink = Array.from(document.querySelectorAll('span'))
.find(el => el.textContent?.includes('Select all'));

if (selectAllConversationsLink instanceof HTMLElement) {
selectAllConversationsLink.click();
await this.wait(500);
}
}

private async clickArchiveButton(): Promise {
const archiveBtn = document.querySelector(this.SELECTORS.archiveButton) as HTMLElement;
if (!archiveBtn) {
throw new Error('Archive button not found');
}

archiveBtn.click();
await this.wait(500);
}

private async waitForArchiveComplete(): Promise {
// Wait for the "archived" confirmation banner
let attempts = 0;
const maxAttempts = 20;

while (attempts < maxAttempts) {
const banner = Array.from(document.querySelectorAll('span'))
.find(el => el.textContent?.toLowerCase().includes('archived'));

if (banner) {
await this.wait(500);
return;
}

await this.wait(500);
attempts++;
}
}

private wait(ms: number): Promise {
return new Promise(resolve => setTimeout(resolve, ms));
}
}
19 changes: 18 additions & 1 deletion src/data/content_scripts/content.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,23 @@
// src/data/content_scripts/content.ts

import { BrowserEmailService } from "../services/browser_email_service";
import { PageInteractionService } from "../services/page_interaction_service";
import { ArchiveAutomation } from './archiveAutomation';

chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.action === 'performArchive') {
const archiver = new ArchiveAutomation();
archiver.archiveEmailsFromSender(message.sender)
.then(result => sendResponse(result))
.catch(error => sendResponse({
success: false,
archivedCount: 0,
error: error.message
}));
return true; // Required for async response
}
// ... existing handlers
});


let currentAbortController: AbortController | null = null;

Expand Down
47 changes: 47 additions & 0 deletions src/presentation/apps/sidebar/App.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import "./App.css";
import { useState } from 'react';
import { useTheme } from "../../providers/theme_provider.tsx";
import { ActionButton } from "./components/actionButton.tsx";
import { ModalPopup } from "./components/modalPopup.tsx";
Expand All @@ -22,6 +23,52 @@ function App() {
);
}

const handleArchive = async (sender: string) => {
setIsArchiving(true);
setArchiveStatus('');

try {
const response = await chrome.runtime.sendMessage({
action: 'archiveEmails',
data: { sender }
});

if (response.success) {
setArchiveStatus(`Successfully archived ${response.result.archivedCount} emails`);
// Optionally refresh the sender list
await refreshSenders();
} else {
setArchiveStatus(`Error: ${response.error}`);
}
} catch (error) {
console.error('Archive error:', error);
setArchiveStatus('Failed to archive emails');
} finally {
setIsArchiving(false);
}
};

// Add Archive button in your table row JSX
// (Next to your Delete/Unsubscribe buttons)
<button
onClick={() => handleArchive(sender.email)}
disabled={isArchiving}
className="px-3 py-1 bg-blue-500 hover:bg-blue-600 text-white rounded text-sm disabled:opacity-50"
title="Archive all emails from this sender"
>
{isArchiving ? (





Archiving...

) : (
'📦 Archive'
)}


function AppWithTheme() {
const { theme } = useTheme();
const { searchTerm, setSearchTerm } = useApp();
Expand Down
9 changes: 9 additions & 0 deletions src/presentation/apps/sidebar/components/actionButton.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,15 @@ export const ActionButton = ({ id }: { id: string }) => {
const { selectedSenders, hideSenders } = useApp();
const { setModal } = useModal();

const [isArchiving, setIsArchiving] = useState(false);
const [archiveStatus, setArchiveStatus] = useState('');

{archiveStatus && (

{archiveStatus}

)}

const handleClick = async () => {
const selectedSenderKeys: string[] = Object.keys(selectedSenders);
if (selectedSenderKeys.length > 0) {
Expand Down
Loading