Skip to content

Commit 5afbac1

Browse files
committed
feat: add comprehensive bulk actions to multi-select action bar
Wire up the existing bulkMarkAsRead, bulkMarkAsUnread, bulkStar, and bulkUnstar functions to the selection action bar, which previously only offered Archive, Delete, and Move. New bulk actions: - Mark as read / Mark as unread (MailOpen / EyeOff icons) - Star / Unstar (Star / StarOff icons) - Report spam / Not spam (ShieldAlert / Inbox icons) - Label as… (Tag icon with dropdown label picker) Add folder-aware derived state (listIsSpamOrJunk, listIsTrashFolder, listIsDraftFolder, spamFolderPath) so each action is conditionally shown based on the current folder — matching Gmail's behavior where irrelevant actions are hidden (e.g. no "Archive" in Trash, "Not spam" replaces "Report spam" in the Junk folder). Add bulkSpam() and bulkNotSpam() helpers that delegate to bulkMoveTo() with the resolved spam/inbox folder path. Add a bulk label dropdown that mirrors the existing toolbar label picker, reusing applyLabelToTargets() and the availableLabelsFromStore list, including the "Create new label" shortcut. Dropdowns (Move, Label) are mutually exclusive — opening one closes the others — and all close on outside click.
1 parent 710936c commit 5afbac1

1 file changed

Lines changed: 184 additions & 2 deletions

File tree

src/svelte/Mailbox.svelte

Lines changed: 184 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -179,6 +179,10 @@
179179
import Sun from '@lucide/svelte/icons/sun';
180180
import Moon from '@lucide/svelte/icons/moon';
181181
import WifiOff from '@lucide/svelte/icons/wifi-off';
182+
import MailOpen from '@lucide/svelte/icons/mail-open';
183+
import EyeOff from '@lucide/svelte/icons/eye-off';
184+
import StarOff from '@lucide/svelte/icons/star-off';
185+
import EllipsisVertical from '@lucide/svelte/icons/ellipsis-vertical';
182186
import EmailIframe from './components/EmailIframe.svelte';
183187
import TabBar from './components/TabBar.svelte';
184188
import MessageTab from './components/MessageTab.svelte';
@@ -531,6 +535,8 @@
531535
mailboxView?.bulkMoveOpen,
532536
false,
533537
);
538+
let bulkLabelOpen = $state(false);
539+
let bulkMoreOpen = $state(false);
534540
let availableMoveTargets = chooseStore(
535541
source.state?.availableMoveTargets,
536542
mailboxView?.availableMoveTargets,
@@ -2156,6 +2162,12 @@
21562162
) {
21572163
labelMenuOpen = false;
21582164
}
2165+
if (bulkLabelOpen && !e.target?.closest?.('[data-bulk-label]')) {
2166+
bulkLabelOpen = false;
2167+
}
2168+
if (bulkMoreOpen && !e.target?.closest?.('[data-bulk-more]')) {
2169+
bulkMoreOpen = false;
2170+
}
21592171
// Close action menu when clicking outside
21602172
if (actionMenuOpen && !e.target?.closest?.('[data-action-menu]')) {
21612173
actionMenuOpen = false;
@@ -3408,6 +3420,24 @@
34083420
});
34093421
};
34103422
3423+
const bulkSpam = async () => {
3424+
const path = spamFolderPath;
3425+
if (!path) {
3426+
showToast('Spam folder not found', 'error');
3427+
return;
3428+
}
3429+
await bulkMoveTo(path);
3430+
};
3431+
3432+
const bulkNotSpam = async () => {
3433+
const path = inboxFolderPath;
3434+
if (!path) {
3435+
showToast('Inbox folder not found', 'error');
3436+
return;
3437+
}
3438+
await bulkMoveTo(path);
3439+
};
3440+
34113441
const openContextMenu = (event, item) => {
34123442
event?.preventDefault?.();
34133443
const isConversation = Array.isArray(item?.messages);
@@ -3629,6 +3659,9 @@
36293659
resolveFolderPath('getArchiveFolderPath', ['ARCHIVE'], $folders),
36303660
);
36313661
const inboxFolderPath = $derived(resolveFolderPath(null, ['INBOX'], $folders));
3662+
const spamFolderPath = $derived(
3663+
resolveFolderPath('getSpamFolderPath', ['SPAM', 'JUNK'], $folders),
3664+
);
36323665
36333666
// ── Mobile tab bar state ──────────────────────────────────────────────────
36343667
const inboxUnseenCount = $derived(($folders || []).find((f) => f.path === 'INBOX')?.count || 0);
@@ -3729,6 +3762,13 @@
37293762
const canNotSpam = $derived(readerIsSpamOrJunk);
37303763
const showReaderMenuDivider = $derived(canReply || canForward || canEditDraft || canToggleRead);
37313764
3765+
// ── Bulk-action bar folder awareness ─────────────────────────────────────
3766+
const listIsSpamOrJunk = $derived(matchesFolderKey($selectedFolder, ['SPAM', 'JUNK']));
3767+
const listIsTrashFolder = $derived(
3768+
matchesFolderKey($selectedFolder, ['TRASH', 'DELETED', 'DELETED ITEMS']),
3769+
);
3770+
const listIsDraftFolder = $derived(isDraftFolder($selectedFolder));
3771+
37323772
const openDraftFromMessage = async (msg) => {
37333773
if (!msg || !mailboxView?.composeModal?.open) return;
37343774
const account = $currentAccount || Local.get('email') || 'default';
@@ -5550,7 +5590,7 @@
55505590
>
55515591
<span>{$selectedConversationIds.length}</span>
55525592
</div>
5553-
<div class="flex items-center gap-1">
5593+
<div class="flex items-center gap-1 flex-wrap">
55545594
<button
55555595
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
55565596
type="button"
@@ -5561,7 +5601,51 @@
55615601
>
55625602
<X class="h-5 w-5" />
55635603
</button>
5564-
{#if $selectedFolder?.toUpperCase?.() !== 'ARCHIVE'}
5604+
{#if !listIsDraftFolder && !listIsTrashFolder && !listIsSpamOrJunk}
5605+
<button
5606+
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
5607+
type="button"
5608+
aria-label="Mark as read"
5609+
data-tooltip="Mark as read"
5610+
data-tooltip-position="bottom"
5611+
onclick={bulkMarkAsRead}
5612+
>
5613+
<MailOpen class="h-5 w-5" />
5614+
</button>
5615+
<button
5616+
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
5617+
type="button"
5618+
aria-label="Mark as unread"
5619+
data-tooltip="Mark as unread"
5620+
data-tooltip-position="bottom"
5621+
onclick={bulkMarkAsUnread}
5622+
>
5623+
<EyeOff class="h-5 w-5" />
5624+
</button>
5625+
{/if}
5626+
{#if !listIsDraftFolder}
5627+
<button
5628+
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
5629+
type="button"
5630+
aria-label="Star selected"
5631+
data-tooltip="Star selected"
5632+
data-tooltip-position="bottom"
5633+
onclick={bulkStar}
5634+
>
5635+
<Star class="h-5 w-5" />
5636+
</button>
5637+
<button
5638+
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
5639+
type="button"
5640+
aria-label="Unstar selected"
5641+
data-tooltip="Unstar selected"
5642+
data-tooltip-position="bottom"
5643+
onclick={bulkUnstar}
5644+
>
5645+
<StarOff class="h-5 w-5" />
5646+
</button>
5647+
{/if}
5648+
{#if !matchesFolderKey( $selectedFolder, ['ARCHIVE'], ) && !listIsSpamOrJunk && !listIsDraftFolder && !listIsTrashFolder}
55655649
<button
55665650
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
55675651
type="button"
@@ -5573,6 +5657,29 @@
55735657
<Archive class="h-5 w-5" />
55745658
</button>
55755659
{/if}
5660+
{#if listIsSpamOrJunk}
5661+
<button
5662+
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
5663+
type="button"
5664+
aria-label="Not spam"
5665+
data-tooltip="Not spam"
5666+
data-tooltip-position="bottom"
5667+
onclick={bulkNotSpam}
5668+
>
5669+
<Inbox class="h-5 w-5" />
5670+
</button>
5671+
{:else if !listIsDraftFolder && !listIsTrashFolder}
5672+
<button
5673+
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
5674+
type="button"
5675+
aria-label="Report spam"
5676+
data-tooltip="Report spam"
5677+
data-tooltip-position="bottom"
5678+
onclick={bulkSpam}
5679+
>
5680+
<ShieldAlert class="h-5 w-5" />
5681+
</button>
5682+
{/if}
55765683
<button
55775684
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
55785685
type="button"
@@ -5591,6 +5698,8 @@
55915698
data-tooltip="Move selected"
55925699
data-tooltip-position="bottom"
55935700
onclick={() => {
5701+
bulkLabelOpen = false;
5702+
bulkMoreOpen = false;
55945703
if (bulkMoveOpen?.update) {
55955704
bulkMoveOpen.update((v) => !v);
55965705
} else if (mailboxView?.toggleBulkMove) {
@@ -5616,6 +5725,79 @@
56165725
</div>
56175726
{/if}
56185727
</div>
5728+
<div class="relative" data-bulk-label>
5729+
<button
5730+
class="inline-flex items-center justify-center h-11 w-11 hover:bg-accent hover:text-accent-foreground"
5731+
type="button"
5732+
aria-label="Label selected"
5733+
aria-expanded={bulkLabelOpen}
5734+
data-tooltip="Label selected"
5735+
data-tooltip-position="bottom"
5736+
onclick={() => {
5737+
bulkMoreOpen = false;
5738+
if (bulkMoveOpen?.set) bulkMoveOpen.set(false);
5739+
bulkLabelOpen = !bulkLabelOpen;
5740+
}}
5741+
>
5742+
<Tag class="h-5 w-5" />
5743+
</button>
5744+
{#if bulkLabelOpen}
5745+
<div
5746+
class="absolute right-0 z-50 mt-1 min-w-[160px] max-h-[300px] overflow-y-auto border border-border bg-popover p-1 shadow-md"
5747+
>
5748+
<div
5749+
class="px-2 py-1 text-xs font-medium text-muted-foreground uppercase tracking-wider"
5750+
>
5751+
Apply label
5752+
</div>
5753+
{#if !availableLabelsFromStore.length}
5754+
<div class="px-3 py-2 text-sm text-muted-foreground">No labels yet.</div>
5755+
{/if}
5756+
{#each availableLabelsFromStore as label}
5757+
{#if label}
5758+
<button
5759+
type="button"
5760+
class={`flex items-center gap-2 w-full px-3 py-2 text-sm transition-colors ${labelState(label) === 'all' ? 'bg-accent text-accent-foreground' : 'hover:bg-accent'}`}
5761+
aria-pressed={labelState(label) === 'all'}
5762+
data-state={labelState(label)}
5763+
onclick={() => {
5764+
applyLabelToTargets(label);
5765+
bulkLabelOpen = false;
5766+
}}
5767+
>
5768+
<span
5769+
class="w-2.5 h-2.5 rounded-full shrink-0"
5770+
style={`background:${label.color || '#9ca3af'}`}
5771+
></span>
5772+
<span class="flex-1 text-left"
5773+
>{label.name || label.label || label.value}</span
5774+
>
5775+
{#if labelState(label) === 'partial'}
5776+
<span class="text-muted-foreground">&bull;</span>
5777+
{:else if labelState(label) === 'all'}
5778+
<Check class="h-4 w-4 shrink-0" />
5779+
{/if}
5780+
</button>
5781+
{/if}
5782+
{/each}
5783+
<div class="my-1 h-px bg-border"></div>
5784+
<button
5785+
type="button"
5786+
class="flex items-center gap-2 w-full px-3 py-2 text-sm transition-colors hover:bg-accent text-muted-foreground"
5787+
onclick={() => {
5788+
openLabelModal();
5789+
bulkLabelOpen = false;
5790+
}}
5791+
>
5792+
<span
5793+
class="w-2.5 h-2.5 rounded-full shrink-0"
5794+
style={`background:${labelFormColor || labelPalette[0]}`}
5795+
></span>
5796+
<span>Create new label</span>
5797+
</button>
5798+
</div>
5799+
{/if}
5800+
</div>
56195801
</div>
56205802
</div>
56215803
{/if}

0 commit comments

Comments
 (0)