From 2c9b402b089f820f65a9b2c88e5164ee07e272ad Mon Sep 17 00:00:00 2001 From: cesnimda Date: Tue, 30 Jun 2026 20:22:28 +0200 Subject: [PATCH] feat: inline unsubscribe button on email rows MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - ✉✕ button appears on rows where the email has a List-Unsubscribe header - One-click POST / HTTP GET executed automatically; shows ✓ on success - mailto: targets open the user mail client (we never auto-send email) - POST /email/{id}/unsubscribe: detects sender, then processes — UserId scoped - EmailSummaryDto gains IsStarred, HasListUnsubscribe, SupportsOneClick fields - Both SearchService and CleanupService updated to populate new DTO fields Co-Authored-By: Claude Sonnet 4.6 --- frontend/src/api/client.js | 1 + frontend/src/components/EmailRow.jsx | 24 ++++++++++++ frontend/src/styles.css | 2 + .../Controllers/EmailController.cs | 38 ++++++++++++++++++- src/InboxIntel.Application/DTOs/EmailDtos.cs | 5 ++- .../Cleanup/CleanupService.cs | 3 +- .../Search/SearchService.cs | 3 +- 7 files changed, 72 insertions(+), 4 deletions(-) diff --git a/frontend/src/api/client.js b/frontend/src/api/client.js index 697024e..cebe4cc 100644 --- a/frontend/src/api/client.js +++ b/frontend/src/api/client.js @@ -105,6 +105,7 @@ export const EmailApi = { unstar: (id) => api.post(`/email/${id}/unstar`), trash: (id) => api.post(`/email/${id}/trash`), untrash: (id) => api.post(`/email/${id}/untrash`), + unsubscribe:(id) => api.post(`/email/${id}/unsubscribe`).then((r) => r.data), }; export const ExportApi = { diff --git a/frontend/src/components/EmailRow.jsx b/frontend/src/components/EmailRow.jsx index ef2be84..8fc63fb 100644 --- a/frontend/src/components/EmailRow.jsx +++ b/frontend/src/components/EmailRow.jsx @@ -32,6 +32,22 @@ export default function EmailRow({ email: initial, onRemove }) { } }; + const handleUnsub = async (e) => { + e.stopPropagation(); + if (acting) return; + setActing(true); + try { + const res = await EmailApi.unsubscribe(email.id); + if (res.method === 'mailto') { + window.location.href = res.target; + } else { + setEmail((prev) => ({ ...prev, _unsubDone: true })); + } + } finally { + setActing(false); + } + }; + const handleTrash = async (e) => { e.stopPropagation(); if (acting) return; @@ -84,6 +100,14 @@ export default function EmailRow({ email: initial, onRemove }) { ? act(EmailApi.unstar, { isStarred: false }) : act(EmailApi.star, { isStarred: true })} >⭐ + {email.hasListUnsubscribe && ( + + )}