diff --git a/stores/__tests__/email-store-plugin-selection-hooks.test.ts b/stores/__tests__/email-store-plugin-selection-hooks.test.ts new file mode 100644 index 000000000..c8322942a --- /dev/null +++ b/stores/__tests__/email-store-plugin-selection-hooks.test.ts @@ -0,0 +1,70 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { useEmailStore } from '../email-store'; +import { clearAllHooks, emailHooks } from '@/lib/plugin-hooks'; +import type { Email, Mailbox } from '@/lib/jmap/types'; + +// onEmailSelectionChange and onMailboxChange follow the store state, so every +// path that changes the selection or the open folder fires them once. + +function makeEmail(id: string, overrides: Partial = {}): Email { + return { id, threadId: `t-${id}`, keywords: {}, mailboxIds: { inbox: true }, ...overrides } as Email; +} + +const mailboxes = [ + { id: 'inbox', name: 'Inbox', role: 'inbox' }, + { id: 'projects', name: 'Projects' }, +] as Mailbox[]; + +beforeEach(() => { + clearAllHooks(); + useEmailStore.setState({ + mailboxes, + accountMailboxes: {}, + viewingAccountId: null, + selectedMailbox: 'inbox', + emails: [makeEmail('a'), makeEmail('b'), makeEmail('c', { sourceClientAccountId: 'x', sourceAccountId: 'y' })], + selectedEmailKeys: new Set(), + lastSelectedEmailKey: null, + listRows: null, + }); +}); + +afterEach(() => clearAllHooks()); + +describe('onEmailSelectionChange', () => { + it('reports the selected ids each time the selection changes', () => { + const changed = vi.fn(); + emailHooks.onEmailSelectionChange.register('t', changed); + const store = useEmailStore.getState(); + + store.toggleEmailSelection(makeEmail('a')); + store.selectRangeEmails(makeEmail('b')); + store.selectAllEmails(); + store.clearSelection(); + // Clearing an empty selection is no change. + store.clearSelection(); + + expect(changed.mock.calls).toEqual([ + [{ emailIds: ['a'] }], + [{ emailIds: ['a', 'b'] }], + [{ emailIds: ['a', 'b', 'c'] }], + [{ emailIds: [] }], + ]); + }); +}); + +describe('onMailboxChange', () => { + it('reports the folder opened, its role and the one left', () => { + const changed = vi.fn(); + emailHooks.onMailboxChange.register('t', changed); + + useEmailStore.getState().selectMailbox('projects'); + useEmailStore.getState().selectMailbox('projects'); + useEmailStore.getState().selectMailbox('inbox'); + + expect(changed.mock.calls).toEqual([ + [{ mailboxId: 'projects', role: null, previousMailboxId: 'inbox' }], + [{ mailboxId: 'inbox', role: 'inbox', previousMailboxId: 'projects' }], + ]); + }); +}); diff --git a/stores/email-store.ts b/stores/email-store.ts index 742889601..19b9820fa 100644 --- a/stores/email-store.ts +++ b/stores/email-store.ts @@ -7,7 +7,7 @@ import { useCalendarStore } from "@/stores/calendar-store"; import { orderKeywords, placeHeldRows, type HeldKeywords, type SortLevel } from "@/lib/message-list-order"; import { SearchFilters, DEFAULT_SEARCH_FILTERS, buildJMAPFilter, isFilterEmpty } from "@/lib/jmap/search-utils"; import { emailHooks } from "@/lib/plugin-hooks"; -import { allowEmailMove, emitEmailMove, keywordChangeLists, mailboxToView, quotaToView } from "@/lib/plugin-mail-hooks"; +import { allowEmailMove, emitEmailMove, idsFromSelectionKeys, keywordChangeLists, mailboxToView, quotaToView } from "@/lib/plugin-mail-hooks"; import { emailSummary } from "@/lib/plugin-projection"; import { resolveThreadRoute } from "@/lib/thread-routing"; import { emailKeyFor, threadKeyFor, threadIdFromKey, KEYWORD_PREFIX, KEYWORD_PREFIX_LEGACY } from "@/lib/thread-utils"; @@ -6385,6 +6385,19 @@ export const useEmailStore = create((set, get) => ({ // every path that changes it (clicks, keyboard, a batch action clearing the // selection, an account switch) fires them once. useEmailStore.subscribe((state, prev) => { + if (state.selectedEmailKeys !== prev.selectedEmailKeys && emailHooks.onEmailSelectionChange.size > 0) { + const changed = state.selectedEmailKeys.size !== prev.selectedEmailKeys.size + || [...state.selectedEmailKeys].some(key => !prev.selectedEmailKeys.has(key)); + if (changed) void emailHooks.onEmailSelectionChange.emit({ emailIds: idsFromSelectionKeys(state.selectedEmailKeys) }); + } + if (state.selectedMailbox !== prev.selectedMailbox && state.selectedMailbox && emailHooks.onMailboxChange.size > 0) { + const mailbox = mailboxesInView(state).find(mb => mb.id === state.selectedMailbox); + void emailHooks.onMailboxChange.emit({ + mailboxId: state.selectedMailbox, + role: mailbox?.role ?? (state.isUnifiedView ? state.unifiedRole : null) ?? null, + previousMailboxId: prev.selectedMailbox || null, + }); + } if (state.quota && state.quota !== prev.quota && (state.quota.used !== prev.quota?.used || state.quota.total !== prev.quota?.total)) { void emailHooks.onQuotaChange.emit(quotaToView(state.quota));