feat: multi-account mail sidebar and client routing for Pro shell
This commit is contained in:
+196
-85
@@ -1,5 +1,5 @@
|
||||
import { create } from "zustand";
|
||||
import { Email, Mailbox, StateChange, isUnifiedMailboxId, UNIFIED_ROLE_BY_ID } from "@/lib/jmap/types";
|
||||
import { Email, Mailbox, StateChange } from "@/lib/jmap/types";
|
||||
import type { UnifiedMailboxRole } from "@/lib/jmap/types";
|
||||
import type { IJMAPClient } from "@/lib/jmap/client-interface";
|
||||
import { useSettingsStore } from "@/stores/settings-store";
|
||||
@@ -200,6 +200,54 @@ function getNextSelectedEmail(state: { emails: Email[]; selectedEmail: Email | n
|
||||
return getNextSelectedEmailAfterRemoval(state, new Set([removedEmailId]));
|
||||
}
|
||||
|
||||
/**
|
||||
* When the mail view is showing a non-active account (Pro shell's
|
||||
* Thunderbird-style sidebar), redirect read/write operations to that
|
||||
* account's JMAP client and mailbox cache. Returns the passed-in values
|
||||
* unchanged for the standard single-account flow.
|
||||
*
|
||||
* Compose/send still routes through the caller's client (the active
|
||||
* account), since identity binding for cross-account sending is a separate
|
||||
* concern.
|
||||
*/
|
||||
function resolveActionClient(passedClient: IJMAPClient): IJMAPClient {
|
||||
const viewingId = useEmailStore.getState().viewingAccountId;
|
||||
if (!viewingId) return passedClient;
|
||||
const c = useAuthStore.getState().getClientForAccount(viewingId);
|
||||
return c ?? passedClient;
|
||||
}
|
||||
|
||||
function resolveActionMailboxes(): Mailbox[] {
|
||||
const state = useEmailStore.getState();
|
||||
if (state.viewingAccountId) {
|
||||
return state.accountMailboxes[state.viewingAccountId] ?? state.mailboxes;
|
||||
}
|
||||
return state.mailboxes;
|
||||
}
|
||||
|
||||
/**
|
||||
* After a mailbox-list mutation (create/rename/delete/etc.), refresh the
|
||||
* cache for whichever account we're operating on. Writes the result to the
|
||||
* standard `mailboxes` slot for the active account, or the per-account
|
||||
* cache for non-active accounts so the Pro sidebar stays in sync.
|
||||
*/
|
||||
async function refreshMailboxesForViewingAccount(fallbackClient: IJMAPClient): Promise<void> {
|
||||
const viewingId = useEmailStore.getState().viewingAccountId;
|
||||
const client = resolveActionClient(fallbackClient);
|
||||
try {
|
||||
const mailboxes = await client.getMailboxes();
|
||||
if (viewingId) {
|
||||
useEmailStore.setState((state) => ({
|
||||
accountMailboxes: { ...state.accountMailboxes, [viewingId]: mailboxes },
|
||||
}));
|
||||
} else {
|
||||
useEmailStore.setState({ mailboxes });
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to refresh mailboxes after mutation:', error);
|
||||
}
|
||||
}
|
||||
|
||||
// Find the trash mailbox for a given account scope. Prefers JMAP role, but
|
||||
// falls back to name matching ("trash" / "deleted") so users with custom or
|
||||
// pre-existing folders (e.g. "Deleted Items") aren't silently destroyed.
|
||||
@@ -322,7 +370,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
return;
|
||||
}
|
||||
const tagIds = keywords.map(k => k.id);
|
||||
const counts = await client.getTagCounts(tagIds);
|
||||
const counts = await resolveActionClient(client).getTagCounts(tagIds);
|
||||
set({ tagCounts: counts });
|
||||
} catch (error) {
|
||||
console.error('Failed to fetch tag counts:', error);
|
||||
@@ -446,9 +494,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
set({ isLoading: true, error: null }); // Keep previous emails visible during transition
|
||||
try {
|
||||
const targetMailboxId = mailboxId || get().selectedMailbox;
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
|
||||
// Find the mailbox to get its accountId (for shared folder support)
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const mailbox = mailboxes.find(mb => mb.id === targetMailboxId);
|
||||
// Only pass accountId for shared mailboxes, not for primary account
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
@@ -464,7 +513,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// When filtering by tag, omit the mailbox constraint so emails across
|
||||
// all folders that carry the tag are returned.
|
||||
const result = await client.getEmails(selectedKeyword ? undefined : jmapMailboxId, accountId, emailsPerPage, 0, keywordFilter);
|
||||
const result = await effectiveClient.getEmails(selectedKeyword ? undefined : jmapMailboxId, accountId, emailsPerPage, 0, keywordFilter);
|
||||
set({
|
||||
emails: result.emails,
|
||||
hasMoreEmails: result.hasMore,
|
||||
@@ -532,6 +581,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
set({ isLoadingMore: true, error: null });
|
||||
try {
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
// Get emails per page from settings
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
|
||||
@@ -544,21 +594,21 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
const hasFilters = !isFilterEmpty(searchFilters);
|
||||
|
||||
if (searchQuery || hasFilters) {
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
|
||||
if (hasFilters) {
|
||||
const filter = buildJMAPFilter(searchQuery, searchFilters, jmapMailboxId);
|
||||
result = await client.advancedSearchEmails(filter, accountId, emailsPerPage, position);
|
||||
result = await effectiveClient.advancedSearchEmails(filter, accountId, emailsPerPage, position);
|
||||
} else {
|
||||
result = await client.searchEmails(searchQuery, jmapMailboxId, accountId, emailsPerPage, position);
|
||||
result = await effectiveClient.searchEmails(searchQuery, jmapMailboxId, accountId, emailsPerPage, position);
|
||||
}
|
||||
} else {
|
||||
// Load more from mailbox
|
||||
// Find the mailbox to get its accountId (for shared folder support)
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||
// Only pass accountId for shared mailboxes, not for primary account
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
@@ -566,7 +616,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||
|
||||
// When filtering by tag, omit the mailbox constraint (same rationale as fetchEmails).
|
||||
result = await client.getEmails(selectedKeyword ? undefined : jmapMailboxId, accountId, emailsPerPage, position, selectedKeyword ? `$label:${selectedKeyword}` : undefined);
|
||||
result = await effectiveClient.getEmails(selectedKeyword ? undefined : jmapMailboxId, accountId, emailsPerPage, position, selectedKeyword ? `$label:${selectedKeyword}` : undefined);
|
||||
}
|
||||
|
||||
// Use fresh state when merging to avoid overwriting concurrent updates
|
||||
@@ -597,13 +647,13 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
try {
|
||||
// Find the selected mailbox to determine accountId (for shared folders)
|
||||
const selectedMailboxId = get().selectedMailbox;
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
|
||||
|
||||
// Only pass accountId for shared mailboxes
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
|
||||
const email = await client.getEmail(emailId, accountId);
|
||||
const email = await resolveActionClient(client).getEmail(emailId, accountId);
|
||||
|
||||
if (email) {
|
||||
set({ selectedEmail: email });
|
||||
@@ -619,7 +669,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
fetchQuota: async (client) => {
|
||||
try {
|
||||
const quota = await client.getQuota();
|
||||
const quota = await resolveActionClient(client).getQuota();
|
||||
set({ quota });
|
||||
} catch {
|
||||
// Don't set error state as quota is optional
|
||||
@@ -666,6 +716,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
if (!email) return;
|
||||
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
|
||||
// Get delete action preference from settings
|
||||
const deleteAction = useSettingsStore.getState().deleteAction;
|
||||
@@ -673,7 +724,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// Determine accountId for shared folders
|
||||
const selectedMailboxId = get().selectedMailbox;
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
|
||||
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
|
||||
|
||||
@@ -690,7 +741,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
if (trashMailbox) {
|
||||
// Use originalId for shared mailboxes if available
|
||||
const trashId = trashMailbox.originalId || trashMailbox.id;
|
||||
await client.moveToTrash(emailId, trashId, accountId);
|
||||
await effectiveClient.moveToTrash(emailId, trashId, accountId);
|
||||
|
||||
// Remove from local state (email moved to trash, not in current view)
|
||||
set((state) => {
|
||||
@@ -737,7 +788,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
|
||||
// Permanent delete
|
||||
await client.deleteEmail(emailId);
|
||||
await effectiveClient.deleteEmail(emailId);
|
||||
|
||||
// Remove from local state and update mailbox counters if needed
|
||||
set((state) => {
|
||||
@@ -811,11 +862,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// Determine accountId for shared folders
|
||||
const selectedMailboxId = get().selectedMailbox;
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailboxId);
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
|
||||
await client.markAsRead(emailId, read, accountId);
|
||||
await resolveActionClient(client).markAsRead(emailId, read, accountId);
|
||||
|
||||
// Update local state including mailbox counters
|
||||
set((state) => {
|
||||
@@ -879,14 +930,15 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
const isUnread = !email.keywords?.$seen;
|
||||
const currentMailboxIds = email.mailboxIds ? Object.keys(email.mailboxIds) : [];
|
||||
|
||||
const { selectedMailbox, mailboxes } = get();
|
||||
const { selectedMailbox } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
|
||||
|
||||
const destMailbox = mailboxes.find(mb => mb.id === destinationMailboxId);
|
||||
const jmapDestId = destMailbox?.originalId || destinationMailboxId;
|
||||
|
||||
await client.moveEmail(emailId, jmapDestId, accountId);
|
||||
await resolveActionClient(client).moveEmail(emailId, jmapDestId, accountId);
|
||||
|
||||
set((state) => {
|
||||
const updatedMailboxes = state.mailboxes.map(mailbox => {
|
||||
@@ -933,7 +985,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
|
||||
try {
|
||||
const { emails, mailboxes, selectedMailbox, isUnifiedView } = get();
|
||||
const { emails, selectedMailbox, isUnifiedView } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const destMailbox = mailboxes.find(mb => mb.id === destinationMailboxId);
|
||||
const jmapDestId = destMailbox?.originalId || destinationMailboxId;
|
||||
const idSet = new Set(emailIds);
|
||||
@@ -955,7 +1008,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
} else {
|
||||
const currentMailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
|
||||
await client.batchMoveEmails(emailIds, jmapDestId, accountId);
|
||||
await resolveActionClient(client).batchMoveEmails(emailIds, jmapDestId, accountId);
|
||||
}
|
||||
|
||||
// Adjust counters and drop moved emails from the current view.
|
||||
@@ -1010,12 +1063,14 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
return;
|
||||
}
|
||||
|
||||
const currentMailbox = state.mailboxes.find(mb => mb.id === state.selectedMailbox);
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
const currentMailbox = mailboxes.find(mb => mb.id === state.selectedMailbox);
|
||||
const accountId = currentMailbox?.isShared ? currentMailbox.accountId : undefined;
|
||||
const destMailbox = state.mailboxes.find(mb => mb.id === destinationMailboxId);
|
||||
const destMailbox = mailboxes.find(mb => mb.id === destinationMailboxId);
|
||||
const jmapDestId = destMailbox?.originalId || destinationMailboxId;
|
||||
|
||||
const thread = await client.getThread(email.threadId, accountId);
|
||||
const thread = await effectiveClient.getThread(email.threadId, accountId);
|
||||
const threadEmailIds = thread?.emailIds?.length ? thread.emailIds : [emailId];
|
||||
|
||||
if (threadEmailIds.length <= 1) {
|
||||
@@ -1023,7 +1078,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
return;
|
||||
}
|
||||
|
||||
await client.batchMoveEmails(threadEmailIds, jmapDestId, accountId);
|
||||
await effectiveClient.batchMoveEmails(threadEmailIds, jmapDestId, accountId);
|
||||
|
||||
const removedEmailIds = new Set(threadEmailIds);
|
||||
set((currentState) => {
|
||||
@@ -1057,7 +1112,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
try {
|
||||
// Get the current mailbox to scope the search
|
||||
const selectedMailbox = get().selectedMailbox;
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||
// Use originalId for shared mailboxes
|
||||
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||
@@ -1066,7 +1121,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// Get emails per page from settings
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
const result = await client.searchEmails(query, jmapMailboxId, accountId, emailsPerPage, 0);
|
||||
const result = await resolveActionClient(client).searchEmails(query, jmapMailboxId, accountId, emailsPerPage, 0);
|
||||
const externals = await emailHooks.onProvideSearchResults.transform([] as ExternalSearchResult[], { query, filters: get().searchFilters });
|
||||
set({
|
||||
emails: result.emails,
|
||||
@@ -1088,7 +1143,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
advancedSearch: async (client) => {
|
||||
const { searchQuery, searchFilters, selectedMailbox, mailboxes, searchAbortController } = get();
|
||||
const { searchQuery, searchFilters, selectedMailbox, searchAbortController } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
|
||||
if (searchAbortController) {
|
||||
searchAbortController.abort();
|
||||
@@ -1111,7 +1167,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
const filter = buildJMAPFilter(searchQuery, searchFilters, jmapMailboxId);
|
||||
const emailsPerPage = useSettingsStore.getState().emailsPerPage;
|
||||
const result = await client.advancedSearchEmails(filter, accountId, emailsPerPage, 0);
|
||||
const result = await resolveActionClient(client).advancedSearchEmails(filter, accountId, emailsPerPage, 0);
|
||||
|
||||
if (controller.signal.aborted) return;
|
||||
|
||||
@@ -1159,7 +1215,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
if (!email) return;
|
||||
|
||||
const isFlagged = email.keywords.$flagged || false;
|
||||
await client.toggleStar(emailId, !isFlagged);
|
||||
await resolveActionClient(client).toggleStar(emailId, !isFlagged);
|
||||
|
||||
// Update local state
|
||||
set((state) => ({
|
||||
@@ -1191,7 +1247,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// Batch operations
|
||||
batchMarkAsRead: async (client, read) => {
|
||||
const { selectedEmailIds, emails, mailboxes } = get();
|
||||
const { selectedEmailIds, emails } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
if (selectedEmailIds.size === 0) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
@@ -1215,7 +1272,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
});
|
||||
await Promise.allSettled(promises);
|
||||
} else {
|
||||
await client.batchMarkAsRead(emailIdsArray, read);
|
||||
await resolveActionClient(client).batchMarkAsRead(emailIdsArray, read);
|
||||
}
|
||||
|
||||
// Update local state
|
||||
@@ -1260,7 +1317,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
batchDelete: async (client, permanent = false) => {
|
||||
const { selectedEmailIds, emails, mailboxes, selectedMailbox } = get();
|
||||
const { selectedEmailIds, emails, selectedMailbox } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
if (selectedEmailIds.size === 0) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
@@ -1423,7 +1481,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
});
|
||||
await Promise.allSettled(promises);
|
||||
} else {
|
||||
await client.batchMoveEmails(emailIdsArray, toMailboxId);
|
||||
await resolveActionClient(client).batchMoveEmails(emailIdsArray, toMailboxId);
|
||||
}
|
||||
|
||||
// Update local state - remove from current view since they moved
|
||||
@@ -1448,7 +1506,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
batchArchive: async (client) => {
|
||||
const { selectedEmailIds, emails, mailboxes, fetchMailboxes } = get();
|
||||
const { selectedEmailIds, emails } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
if (selectedEmailIds.size === 0) return;
|
||||
|
||||
const archiveMailbox = mailboxes.find(m => m.role === 'archive' || m.name.toLowerCase() === 'archive');
|
||||
@@ -1462,7 +1521,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
try {
|
||||
await client.batchArchiveEmails(
|
||||
await resolveActionClient(client).batchArchiveEmails(
|
||||
selected.map(e => ({ id: e.id, receivedAt: e.receivedAt })),
|
||||
archiveId,
|
||||
mode,
|
||||
@@ -1473,8 +1532,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
const remaining = emails.filter(e => !selectedEmailIds.has(e.id));
|
||||
set({ emails: remaining, selectedEmailIds: new Set(), isLoading: false });
|
||||
|
||||
await fetchMailboxes(client);
|
||||
// Refresh the current mailbox view (honors active search/filters)
|
||||
// Refresh the active or viewed account's mailbox cache after the
|
||||
// archive (a year/month archive can create new sub-folders).
|
||||
await refreshMailboxesForViewingAccount(client);
|
||||
await get().refreshCurrentMailbox(client);
|
||||
} catch (error) {
|
||||
set({
|
||||
@@ -1487,7 +1547,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// Spam operations
|
||||
markAsSpam: async (client, emailId) => {
|
||||
const { selectedMailbox, mailboxes, emails } = get();
|
||||
const { selectedMailbox, emails } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const email = emails.find(e => e.id === emailId);
|
||||
if (!email) return;
|
||||
|
||||
@@ -1501,7 +1562,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
});
|
||||
|
||||
try {
|
||||
await client.markAsSpam(emailId, currentMailbox.accountId);
|
||||
await resolveActionClient(client).markAsSpam(emailId, currentMailbox.accountId);
|
||||
|
||||
set(state => ({
|
||||
emails: state.emails.filter(e => e.id !== emailId),
|
||||
@@ -1514,7 +1575,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
undoSpam: async (client, emailId) => {
|
||||
const { mailboxes, selectedMailbox } = get();
|
||||
const { selectedMailbox } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
|
||||
// Try cache first (preserves exact original mailbox for toast undo)
|
||||
const cachedData = get().spamUndoCache.get(emailId);
|
||||
@@ -1546,7 +1608,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
}
|
||||
|
||||
try {
|
||||
await client.undoSpam(emailId, targetMailboxId, accountId);
|
||||
await resolveActionClient(client).undoSpam(emailId, targetMailboxId, accountId);
|
||||
await get().fetchEmails(client, selectedMailbox);
|
||||
} catch (error) {
|
||||
console.error('Failed to restore email:', error);
|
||||
@@ -1555,14 +1617,16 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
batchMarkAsSpam: async (client, emailIds) => {
|
||||
const { selectedMailbox, mailboxes } = get();
|
||||
const { selectedMailbox } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
|
||||
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
||||
if (!currentMailbox) return;
|
||||
|
||||
try {
|
||||
for (const emailId of emailIds) {
|
||||
await client.markAsSpam(emailId, currentMailbox.accountId);
|
||||
await effectiveClient.markAsSpam(emailId, currentMailbox.accountId);
|
||||
}
|
||||
|
||||
set(state => ({
|
||||
@@ -1577,7 +1641,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
batchUndoSpam: async (client: IJMAPClient, emailIds: string[]) => {
|
||||
const { mailboxes, selectedMailbox } = get();
|
||||
const { selectedMailbox } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
|
||||
// Find inbox (batch operations don't preserve original mailboxes)
|
||||
const currentMailbox = mailboxes.find(m => m.id === selectedMailbox);
|
||||
@@ -1594,7 +1660,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
try {
|
||||
for (const emailId of emailIds) {
|
||||
await client.undoSpam(emailId, inboxMailbox.originalId || inboxMailbox.id, accountId);
|
||||
await effectiveClient.undoSpam(emailId, inboxMailbox.originalId || inboxMailbox.id, accountId);
|
||||
}
|
||||
|
||||
set(state => ({
|
||||
@@ -1681,7 +1747,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
try {
|
||||
// Fetch emails for the current mailbox without clearing the list first
|
||||
// This provides a smoother update experience
|
||||
const mailboxes = get().mailboxes;
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
const mailbox = mailboxes.find(mb => mb.id === selectedMailbox);
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
const jmapMailboxId = mailbox?.originalId || selectedMailbox;
|
||||
@@ -1697,9 +1764,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
let result;
|
||||
if (hasFilters || searchQuery) {
|
||||
const filter = buildJMAPFilter(searchQuery, searchFilters, jmapMailboxId);
|
||||
result = await client.advancedSearchEmails(filter, accountId, emailsPerPage, 0);
|
||||
result = await effectiveClient.advancedSearchEmails(filter, accountId, emailsPerPage, 0);
|
||||
} else {
|
||||
result = await client.getEmails(jmapMailboxId, accountId, emailsPerPage, 0);
|
||||
result = await effectiveClient.getEmails(jmapMailboxId, accountId, emailsPerPage, 0);
|
||||
}
|
||||
|
||||
const currentEmails = get().emails;
|
||||
@@ -1789,7 +1856,8 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
},
|
||||
|
||||
fetchThreadEmails: async (client, threadId) => {
|
||||
const { threadEmailsCache, selectedMailbox, mailboxes } = get();
|
||||
const { threadEmailsCache, selectedMailbox } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
|
||||
// Check if we already have this thread cached
|
||||
const cachedEmails = threadEmailsCache.get(threadId);
|
||||
@@ -1806,7 +1874,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
|
||||
// Fetch all emails in the thread
|
||||
const emails = await client.getThreadEmails(threadId, accountId);
|
||||
const emails = await resolveActionClient(client).getThreadEmails(threadId, accountId);
|
||||
|
||||
// Update cache
|
||||
const newCache = new Map(get().threadEmailsCache);
|
||||
@@ -1841,8 +1909,12 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// Mailbox management
|
||||
createMailbox: async (client, name, parentId) => {
|
||||
try {
|
||||
await client.createMailbox(name, parentId);
|
||||
await get().fetchMailboxes(client);
|
||||
await resolveActionClient(client).createMailbox(name, parentId);
|
||||
if (get().viewingAccountId) {
|
||||
await refreshMailboxesForViewingAccount(client);
|
||||
} else {
|
||||
await get().fetchMailboxes(client);
|
||||
}
|
||||
} catch (error) {
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to create folder' });
|
||||
throw error;
|
||||
@@ -1851,12 +1923,24 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
renameMailbox: async (client, mailboxId, name) => {
|
||||
try {
|
||||
await client.updateMailbox(mailboxId, { name });
|
||||
set({
|
||||
mailboxes: get().mailboxes.map(mb =>
|
||||
mb.id === mailboxId ? { ...mb, name } : mb
|
||||
),
|
||||
});
|
||||
await resolveActionClient(client).updateMailbox(mailboxId, { name });
|
||||
const viewingId = get().viewingAccountId;
|
||||
if (viewingId) {
|
||||
set((state) => ({
|
||||
accountMailboxes: {
|
||||
...state.accountMailboxes,
|
||||
[viewingId]: (state.accountMailboxes[viewingId] ?? []).map(mb =>
|
||||
mb.id === mailboxId ? { ...mb, name } : mb
|
||||
),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
set({
|
||||
mailboxes: get().mailboxes.map(mb =>
|
||||
mb.id === mailboxId ? { ...mb, name } : mb
|
||||
),
|
||||
});
|
||||
}
|
||||
} catch (error) {
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to rename folder' });
|
||||
throw error;
|
||||
@@ -1865,18 +1949,27 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
deleteMailbox: async (client, mailboxId) => {
|
||||
try {
|
||||
await client.deleteMailbox(mailboxId);
|
||||
const { mailboxes, selectedMailbox } = get();
|
||||
const newMailboxes = mailboxes.filter(mb => mb.id !== mailboxId);
|
||||
const updates: Partial<EmailStore> = { mailboxes: newMailboxes };
|
||||
// If the deleted mailbox was selected, switch to inbox
|
||||
if (selectedMailbox === mailboxId) {
|
||||
const inbox = newMailboxes.find(mb => mb.role === 'inbox' && !mb.isShared);
|
||||
if (inbox) {
|
||||
updates.selectedMailbox = inbox.id;
|
||||
await resolveActionClient(client).deleteMailbox(mailboxId);
|
||||
const { selectedMailbox, viewingAccountId: viewingId } = get();
|
||||
if (viewingId) {
|
||||
const updatedList = (get().accountMailboxes[viewingId] ?? []).filter(mb => mb.id !== mailboxId);
|
||||
const patch: Partial<EmailStore> = {
|
||||
accountMailboxes: { ...get().accountMailboxes, [viewingId]: updatedList },
|
||||
};
|
||||
if (selectedMailbox === mailboxId) {
|
||||
const inbox = updatedList.find(mb => mb.role === 'inbox' && !mb.isShared);
|
||||
if (inbox) patch.selectedMailbox = inbox.id;
|
||||
}
|
||||
set(patch);
|
||||
} else {
|
||||
const newMailboxes = get().mailboxes.filter(mb => mb.id !== mailboxId);
|
||||
const updates: Partial<EmailStore> = { mailboxes: newMailboxes };
|
||||
if (selectedMailbox === mailboxId) {
|
||||
const inbox = newMailboxes.find(mb => mb.role === 'inbox' && !mb.isShared);
|
||||
if (inbox) updates.selectedMailbox = inbox.id;
|
||||
}
|
||||
set(updates);
|
||||
}
|
||||
set(updates as EmailStore);
|
||||
} catch (error) {
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to delete folder' });
|
||||
throw error;
|
||||
@@ -1885,15 +1978,20 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
setMailboxRole: async (client, mailboxId, role) => {
|
||||
try {
|
||||
const effectiveClient = resolveActionClient(client);
|
||||
// If assigning a role, first clear that role from ALL other mailboxes that have it
|
||||
if (role) {
|
||||
const existingMailboxes = get().mailboxes.filter(mb => mb.role === role && !mb.isShared && mb.id !== mailboxId);
|
||||
const existingMailboxes = resolveActionMailboxes().filter(mb => mb.role === role && !mb.isShared && mb.id !== mailboxId);
|
||||
for (const existing of existingMailboxes) {
|
||||
await client.updateMailbox(existing.id, { role: null });
|
||||
await effectiveClient.updateMailbox(existing.id, { role: null });
|
||||
}
|
||||
}
|
||||
await client.updateMailbox(mailboxId, { role });
|
||||
await get().fetchMailboxes(client);
|
||||
await effectiveClient.updateMailbox(mailboxId, { role });
|
||||
if (get().viewingAccountId) {
|
||||
await refreshMailboxesForViewingAccount(client);
|
||||
} else {
|
||||
await get().fetchMailboxes(client);
|
||||
}
|
||||
} catch (error) {
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to update folder role' });
|
||||
throw error;
|
||||
@@ -1903,7 +2001,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
emptyMailbox: async (client, mailboxId) => {
|
||||
try {
|
||||
set({ isLoading: true, error: null });
|
||||
await client.emptyMailbox(mailboxId);
|
||||
await resolveActionClient(client).emptyMailbox(mailboxId);
|
||||
|
||||
// Clear emails from local state if we're viewing this mailbox
|
||||
const currentMailbox = get().selectedMailbox;
|
||||
@@ -1911,15 +2009,28 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
set({ emails: [], selectedEmail: null });
|
||||
}
|
||||
|
||||
// Update mailbox counters
|
||||
set({
|
||||
mailboxes: get().mailboxes.map(mb =>
|
||||
mb.id === mailboxId
|
||||
? { ...mb, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0 }
|
||||
: mb
|
||||
),
|
||||
isLoading: false,
|
||||
});
|
||||
const viewingId = get().viewingAccountId;
|
||||
if (viewingId) {
|
||||
set((state) => ({
|
||||
accountMailboxes: {
|
||||
...state.accountMailboxes,
|
||||
[viewingId]: (state.accountMailboxes[viewingId] ?? []).map(mb =>
|
||||
mb.id === mailboxId
|
||||
? { ...mb, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0 }
|
||||
: mb
|
||||
),
|
||||
},
|
||||
}));
|
||||
} else {
|
||||
set({
|
||||
mailboxes: get().mailboxes.map(mb =>
|
||||
mb.id === mailboxId
|
||||
? { ...mb, totalEmails: 0, unreadEmails: 0, totalThreads: 0, unreadThreads: 0 }
|
||||
: mb
|
||||
),
|
||||
});
|
||||
}
|
||||
set({ isLoading: false });
|
||||
} catch (error) {
|
||||
set({
|
||||
error: error instanceof Error ? error.message : 'Failed to empty folder',
|
||||
@@ -1931,11 +2042,11 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
markMailboxAsRead: async (client, mailboxId) => {
|
||||
try {
|
||||
const mailbox = get().mailboxes.find(mb => mb.id === mailboxId);
|
||||
const mailbox = resolveActionMailboxes().find(mb => mb.id === mailboxId);
|
||||
const accountId = mailbox?.isShared ? mailbox.accountId : undefined;
|
||||
const jmapMailboxId = mailbox?.originalId || mailboxId;
|
||||
|
||||
const count = await client.markMailboxAsRead(jmapMailboxId, accountId);
|
||||
const count = await resolveActionClient(client).markMailboxAsRead(jmapMailboxId, accountId);
|
||||
|
||||
// Update local state: mark all emails currently visible in this mailbox as read,
|
||||
// and zero-out the mailbox unread counter.
|
||||
|
||||
Reference in New Issue
Block a user