Lets users name and pin a search query from the SearchResults page;
saved searches persist to localStorage and show as quick links in a new
sidebar section, similar to Favorites. Clicking re-runs the query.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
'/' focuses the search bar from anywhere (Layout-level listener).
j/k move focus up/down the email list in FolderView and SearchResults;
e archives the focused email, # trashes it. All shortcuts are ignored
while an input/textarea/select has focus, so typing is never hijacked.
A small hint footer in the bottom-right reminds users of the shortcuts.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Select multiple rows (checkbox per row) in FolderView, SearchResults,
and the Senders email panel. A toolbar appears with mark read/unread,
star, archive, and trash, applied to the whole selection via
CleanupService.ExecuteAsync (already scoped to UserId).
Shared via useSelection hook and BulkToolbar component to avoid
duplicating selection state across the three list views.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- EmailRow shared component with hover action buttons: mark read/unread, star/unstar, trash
- Optimistic UI — local state patches immediately on click; row fades and becomes non-interactive during the API call
- Trashed rows disappear from the current folder view via onRemove callback
- Backend: EmailController single-email endpoints (POST /email/{id}/read|unread|star|unstar|trash|untrash)
- Star/Unstar added to CleanupActionType enum and CleanupService (maps to STARRED Gmail label via BatchModifyAsync)
- UserId scope enforced in ResolveTargetsAsync — a user can only act on their own emails
- action buttons use stopPropagation so clicking them does not open Gmail
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Search form in topbar routes to /app/search?q=...
- SearchResults page with infinite scroll, same row style as FolderView
- Supports Gmail-like operators: from:, is:unread, has:attachment, after:, before:
- SearchApi.query() calls GET /search which runs through the existing GmailQueryParser
- Input sanitised via encodeURIComponent on submit; external links use noopener/noreferrer
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>