fix: route all counter updates to the email's own account in aggregate views
Extend the counter-routing fix beyond markAsRead to every optimistic mailbox counter update, so a different account's email never adjusts the active account's folder counters (JMAP ids can collide across accounts). - Add applyBatchMailboxCounterUpdate() + applyDeleteCounters() and apply the per-account routing to: deleteEmail (trash + permanent), moveToMailbox, moveEmailsToMailbox, batchMarkAsRead, batchDelete, and markThreadAsRead. - markAsSpam/batchMarkAsSpam/batchMoveToMailbox don't touch counters (rely on refresh) and folder-level ops (rename/empty/markMailboxAsRead) are already account-scoped — left as-is. - Test: batchMarkAsRead adjusts each account's counter in its own list.
This commit is contained in:
@@ -53,6 +53,7 @@ function makeClient() {
|
|||||||
markAsRead: vi.fn().mockResolvedValue(undefined),
|
markAsRead: vi.fn().mockResolvedValue(undefined),
|
||||||
toggleStar: vi.fn().mockResolvedValue(undefined),
|
toggleStar: vi.fn().mockResolvedValue(undefined),
|
||||||
moveEmail: vi.fn().mockResolvedValue(undefined),
|
moveEmail: vi.fn().mockResolvedValue(undefined),
|
||||||
|
batchMarkAsRead: vi.fn().mockResolvedValue(undefined),
|
||||||
} as unknown as IJMAPClient;
|
} as unknown as IJMAPClient;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -146,6 +147,27 @@ describe('unified-view single-email action routing (#281)', () => {
|
|||||||
expect(s.accountMailboxes['account-a'][0].unreadEmails).toBe(5); // active list untouched
|
expect(s.accountMailboxes['account-a'][0].unreadEmails).toBe(5); // active list untouched
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('batchMarkAsRead adjusts each email’s own account counter (cross-account)', async () => {
|
||||||
|
useEmailStore.setState({
|
||||||
|
mailboxes: [makeMailbox({ id: 'inbox', role: 'inbox', unreadEmails: 5 })],
|
||||||
|
accountMailboxes: {
|
||||||
|
'account-a': [makeMailbox({ id: 'inbox', role: 'inbox', unreadEmails: 5 })],
|
||||||
|
'account-b': [makeMailbox({ id: 'inbox', role: 'inbox', unreadEmails: 3 })],
|
||||||
|
},
|
||||||
|
emails: [
|
||||||
|
makeEmail({ id: 'a1', sourceClientAccountId: 'account-a', sourceAccountId: 'account-a', keywords: {}, mailboxIds: { inbox: true } }),
|
||||||
|
makeEmail({ id: 'b1', sourceClientAccountId: 'account-b', sourceAccountId: 'account-b', keywords: {}, mailboxIds: { inbox: true } }),
|
||||||
|
],
|
||||||
|
selectedEmailIds: new Set(['a1', 'b1']),
|
||||||
|
});
|
||||||
|
|
||||||
|
await useEmailStore.getState().batchMarkAsRead(activeClient, true);
|
||||||
|
|
||||||
|
const s = useEmailStore.getState();
|
||||||
|
expect(s.mailboxes[0].unreadEmails).toBe(4); // active account: -1
|
||||||
|
expect(s.accountMailboxes['account-b'][0].unreadEmails).toBe(2); // account-b: -1
|
||||||
|
});
|
||||||
|
|
||||||
it('routes a shared/group email through the delegating login client + owner accountId', async () => {
|
it('routes a shared/group email through the delegating login client + owner accountId', async () => {
|
||||||
await useEmailStore.getState().markAsRead(activeClient, 'email-shared', true);
|
await useEmailStore.getState().markAsRead(activeClient, 'email-shared', true);
|
||||||
// Reached via account-a's client (the active one), targeting the owner account.
|
// Reached via account-a's client (the active one), targeting the owner account.
|
||||||
|
|||||||
+123
-124
@@ -550,6 +550,60 @@ function applyMailboxCounterUpdate(
|
|||||||
return { mailboxes: state.mailboxes.map(adjust) };
|
return { mailboxes: state.mailboxes.map(adjust) };
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Batch variant of applyMailboxCounterUpdate: a set of emails may span accounts,
|
||||||
|
// so group them by the list that holds them and run `mapMailbox` per list using
|
||||||
|
// only that group's emails. Each account's counters are adjusted in its own list
|
||||||
|
// (active account → `mailboxes`, others → `accountMailboxes[sourceClientAccountId]`),
|
||||||
|
// avoiding cross-account id collisions. (#281)
|
||||||
|
function applyBatchMailboxCounterUpdate(
|
||||||
|
state: { mailboxes: Mailbox[]; accountMailboxes: Record<string, Mailbox[]> },
|
||||||
|
emails: Array<{ sourceClientAccountId?: string }>,
|
||||||
|
mapMailbox: (mb: Mailbox, emailsForList: Array<{ sourceClientAccountId?: string }>) => Mailbox,
|
||||||
|
): { mailboxes: Mailbox[]; accountMailboxes: Record<string, Mailbox[]> } {
|
||||||
|
const activeId = useAuthStore.getState().activeAccountId;
|
||||||
|
const ACTIVE = '__active__';
|
||||||
|
const groups = new Map<string, Array<{ sourceClientAccountId?: string }>>();
|
||||||
|
for (const e of emails) {
|
||||||
|
const k = e.sourceClientAccountId && e.sourceClientAccountId !== activeId ? e.sourceClientAccountId : ACTIVE;
|
||||||
|
const g = groups.get(k);
|
||||||
|
if (g) g.push(e); else groups.set(k, [e]);
|
||||||
|
}
|
||||||
|
let mailboxes = state.mailboxes;
|
||||||
|
const accountMailboxes = { ...state.accountMailboxes };
|
||||||
|
for (const [k, es] of groups) {
|
||||||
|
if (k === ACTIVE) {
|
||||||
|
mailboxes = mailboxes.map((mb) => mapMailbox(mb, es));
|
||||||
|
} else if (accountMailboxes[k]) {
|
||||||
|
accountMailboxes[k] = accountMailboxes[k].map((mb) => mapMailbox(mb, es));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { mailboxes, accountMailboxes };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Per-mailbox counter map (for applyBatchMailboxCounterUpdate) for removing a
|
||||||
|
// group of emails from a folder: decrement total (and unread for unseen) for
|
||||||
|
// each group email that lives in the mailbox.
|
||||||
|
function applyDeleteCounters(
|
||||||
|
mailbox: Mailbox,
|
||||||
|
group: Array<{ sourceClientAccountId?: string }>,
|
||||||
|
): Mailbox {
|
||||||
|
let dTotal = 0;
|
||||||
|
let dUnread = 0;
|
||||||
|
for (const email of group as Email[]) {
|
||||||
|
if (emailInMailbox(email, mailbox)) {
|
||||||
|
dTotal--;
|
||||||
|
if (!email.keywords?.$seen) dUnread--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dTotal === 0 && dUnread === 0 ? mailbox : {
|
||||||
|
...mailbox,
|
||||||
|
totalEmails: Math.max(0, mailbox.totalEmails + dTotal),
|
||||||
|
unreadEmails: Math.max(0, mailbox.unreadEmails + dUnread),
|
||||||
|
totalThreads: Math.max(0, mailbox.totalThreads + dTotal),
|
||||||
|
unreadThreads: Math.max(0, mailbox.unreadThreads + dUnread),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Find the trash mailbox for a given account scope. Prefers JMAP role, but
|
// 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
|
// falls back to name matching ("trash" / "deleted") so users with custom or
|
||||||
// pre-existing folders (e.g. "Deleted Items") aren't silently destroyed.
|
// pre-existing folders (e.g. "Deleted Items") aren't silently destroyed.
|
||||||
@@ -1217,40 +1271,38 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
// After marking read in the same request, the email arrives in trash as read.
|
// After marking read in the same request, the email arrives in trash as read.
|
||||||
const arrivesUnread = isUnread && !alsoMarkRead;
|
const arrivesUnread = isUnread && !alsoMarkRead;
|
||||||
|
|
||||||
// Remove from local state (email moved to trash, not in current view)
|
// Remove from local state (email moved to trash, not in current view).
|
||||||
|
// Counter changes go to the email's own account list (#281). Source and
|
||||||
|
// trash live in the same account.
|
||||||
set((state) => {
|
set((state) => {
|
||||||
let updatedMailboxes = state.mailboxes;
|
const mailboxPatch = email.mailboxIds
|
||||||
|
? applyMailboxCounterUpdate(state, email, (mailbox) => {
|
||||||
// Update counters for source mailbox (email leaving)
|
|
||||||
if (email.mailboxIds) {
|
|
||||||
updatedMailboxes = state.mailboxes.map(mailbox => {
|
|
||||||
if (emailInMailbox(email, mailbox)) {
|
if (emailInMailbox(email, mailbox)) {
|
||||||
return {
|
return {
|
||||||
...mailbox,
|
...mailbox,
|
||||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||||
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
||||||
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
||||||
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads
|
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
// Update trash mailbox counters (email arriving)
|
|
||||||
if (mailbox.id === trashMailbox.id) {
|
if (mailbox.id === trashMailbox.id) {
|
||||||
return {
|
return {
|
||||||
...mailbox,
|
...mailbox,
|
||||||
totalEmails: mailbox.totalEmails + 1,
|
totalEmails: mailbox.totalEmails + 1,
|
||||||
unreadEmails: arrivesUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
unreadEmails: arrivesUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
||||||
totalThreads: mailbox.totalThreads + 1,
|
totalThreads: mailbox.totalThreads + 1,
|
||||||
unreadThreads: arrivesUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads
|
unreadThreads: arrivesUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return mailbox;
|
return mailbox;
|
||||||
});
|
})
|
||||||
}
|
: {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
emails: state.emails.filter(e => e.id !== emailId),
|
emails: state.emails.filter(e => e.id !== emailId),
|
||||||
selectedEmail: getNextSelectedEmail(state, emailId),
|
selectedEmail: getNextSelectedEmail(state, emailId),
|
||||||
mailboxes: updatedMailboxes
|
...mailboxPatch,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
return;
|
return;
|
||||||
@@ -1264,42 +1316,26 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
// Permanent delete
|
// Permanent delete
|
||||||
await effectiveClient.deleteEmail(emailId);
|
await effectiveClient.deleteEmail(emailId);
|
||||||
|
|
||||||
// Remove from local state and update mailbox counters if needed
|
// Remove from local state and update mailbox counters (in the email's own
|
||||||
|
// account list). Unread emails also decrement the unread counters. (#281)
|
||||||
set((state) => {
|
set((state) => {
|
||||||
let updatedMailboxes = state.mailboxes;
|
const mailboxPatch = email.mailboxIds
|
||||||
|
? applyMailboxCounterUpdate(state, email, (mailbox) =>
|
||||||
// If the email was unread, decrement the unread counters
|
emailInMailbox(email, mailbox)
|
||||||
if (isUnread && email.mailboxIds) {
|
? {
|
||||||
updatedMailboxes = state.mailboxes.map(mailbox => {
|
|
||||||
if (emailInMailbox(email, mailbox)) {
|
|
||||||
return {
|
|
||||||
...mailbox,
|
...mailbox,
|
||||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||||
unreadEmails: Math.max(0, mailbox.unreadEmails - 1),
|
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
||||||
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
||||||
unreadThreads: Math.max(0, mailbox.unreadThreads - 1)
|
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads,
|
||||||
};
|
|
||||||
}
|
|
||||||
return mailbox;
|
|
||||||
});
|
|
||||||
} else if (email.mailboxIds) {
|
|
||||||
// If email was read, only decrement total counters
|
|
||||||
updatedMailboxes = state.mailboxes.map(mailbox => {
|
|
||||||
if (emailInMailbox(email, mailbox)) {
|
|
||||||
return {
|
|
||||||
...mailbox,
|
|
||||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
|
||||||
totalThreads: Math.max(0, mailbox.totalThreads - 1)
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return mailbox;
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
: mailbox)
|
||||||
|
: {};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
emails: state.emails.filter(e => e.id !== emailId),
|
emails: state.emails.filter(e => e.id !== emailId),
|
||||||
selectedEmail: getNextSelectedEmail(state, emailId),
|
selectedEmail: getNextSelectedEmail(state, emailId),
|
||||||
mailboxes: updatedMailboxes
|
...mailboxPatch,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1415,7 +1451,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
if (!email) return;
|
if (!email) return;
|
||||||
|
|
||||||
const isUnread = !email.keywords?.$seen;
|
const isUnread = !email.keywords?.$seen;
|
||||||
const currentMailboxIds = email.mailboxIds ? Object.keys(email.mailboxIds) : [];
|
|
||||||
|
|
||||||
// In unified view route to the email's own account (client + that
|
// In unified view route to the email's own account (client + that
|
||||||
// account's mailbox list, where the destination id lives); otherwise the
|
// account's mailbox list, where the destination id lives); otherwise the
|
||||||
@@ -1428,14 +1463,16 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
await actionClient.moveEmail(emailId, jmapDestId, accountId);
|
await actionClient.moveEmail(emailId, jmapDestId, accountId);
|
||||||
|
|
||||||
set((state) => {
|
set((state) => {
|
||||||
const updatedMailboxes = state.mailboxes.map(mailbox => {
|
// Counter changes go to the email's own account list; source and
|
||||||
if (currentMailboxIds.includes(mailbox.id) || (!mailbox.isShared && mailbox.originalId ? currentMailboxIds.includes(mailbox.originalId) : false)) {
|
// destination live in the same account. (#281)
|
||||||
|
const mailboxPatch = applyMailboxCounterUpdate(state, email, (mailbox) => {
|
||||||
|
if (emailInMailbox(email, mailbox)) {
|
||||||
return {
|
return {
|
||||||
...mailbox,
|
...mailbox,
|
||||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||||
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
||||||
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
||||||
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads
|
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
if (mailbox.id === destinationMailboxId) {
|
if (mailbox.id === destinationMailboxId) {
|
||||||
@@ -1444,7 +1481,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
totalEmails: mailbox.totalEmails + 1,
|
totalEmails: mailbox.totalEmails + 1,
|
||||||
unreadEmails: isUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
unreadEmails: isUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
||||||
totalThreads: mailbox.totalThreads + 1,
|
totalThreads: mailbox.totalThreads + 1,
|
||||||
unreadThreads: isUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads
|
unreadThreads: isUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return mailbox;
|
return mailbox;
|
||||||
@@ -1453,7 +1490,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
emails: state.emails.filter(e => e.id !== emailId),
|
emails: state.emails.filter(e => e.id !== emailId),
|
||||||
selectedEmail: getNextSelectedEmail(state, emailId),
|
selectedEmail: getNextSelectedEmail(state, emailId),
|
||||||
mailboxes: updatedMailboxes
|
...mailboxPatch,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1506,14 +1543,33 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
|
|
||||||
// Adjust counters and drop moved emails from the current view.
|
// Adjust counters and drop moved emails from the current view.
|
||||||
let unreadDelta = 0;
|
let unreadDelta = 0;
|
||||||
const sourceMailboxIds = new Set<string>();
|
|
||||||
for (const e of affected) {
|
for (const e of affected) {
|
||||||
if (!e.keywords?.$seen) unreadDelta += 1;
|
if (!e.keywords?.$seen) unreadDelta += 1;
|
||||||
if (e.mailboxIds) for (const mid of Object.keys(e.mailboxIds)) sourceMailboxIds.add(mid);
|
|
||||||
}
|
}
|
||||||
const movedCount = affected.length;
|
const movedCount = affected.length;
|
||||||
|
|
||||||
set((state) => ({
|
set((state) => {
|
||||||
|
// Decrement source folders per the email's own account list (#281).
|
||||||
|
const patch = applyBatchMailboxCounterUpdate(state, affected, (mb, group) => {
|
||||||
|
let dTotal = 0;
|
||||||
|
let dUnread = 0;
|
||||||
|
for (const e of group as Email[]) {
|
||||||
|
if (emailInMailbox(e, mb)) {
|
||||||
|
dTotal--;
|
||||||
|
if (!e.keywords?.$seen) dUnread--;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return dTotal === 0 && dUnread === 0 ? mb : {
|
||||||
|
...mb,
|
||||||
|
totalEmails: Math.max(0, mb.totalEmails + dTotal),
|
||||||
|
unreadEmails: Math.max(0, mb.unreadEmails + dUnread),
|
||||||
|
};
|
||||||
|
});
|
||||||
|
// The destination folder (picked from the active/viewing list) gains them.
|
||||||
|
patch.mailboxes = patch.mailboxes.map(mb => mb.id === destinationMailboxId
|
||||||
|
? { ...mb, totalEmails: mb.totalEmails + movedCount, unreadEmails: mb.unreadEmails + unreadDelta }
|
||||||
|
: mb);
|
||||||
|
return {
|
||||||
emails: state.emails.filter(e => !idSet.has(e.id)),
|
emails: state.emails.filter(e => !idSet.has(e.id)),
|
||||||
selectedEmail: state.selectedEmail && idSet.has(state.selectedEmail.id) ? null : state.selectedEmail,
|
selectedEmail: state.selectedEmail && idSet.has(state.selectedEmail.id) ? null : state.selectedEmail,
|
||||||
selectedEmailIds: (() => {
|
selectedEmailIds: (() => {
|
||||||
@@ -1521,24 +1577,9 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
for (const id of idSet) next.delete(id);
|
for (const id of idSet) next.delete(id);
|
||||||
return next;
|
return next;
|
||||||
})(),
|
})(),
|
||||||
mailboxes: state.mailboxes.map(mb => {
|
...patch,
|
||||||
if (sourceMailboxIds.has(mb.id) || (!mb.isShared && mb.originalId ? sourceMailboxIds.has(mb.originalId) : false)) {
|
|
||||||
return {
|
|
||||||
...mb,
|
|
||||||
totalEmails: Math.max(0, mb.totalEmails - movedCount),
|
|
||||||
unreadEmails: Math.max(0, mb.unreadEmails - unreadDelta),
|
|
||||||
};
|
};
|
||||||
}
|
});
|
||||||
if (mb.id === destinationMailboxId) {
|
|
||||||
return {
|
|
||||||
...mb,
|
|
||||||
totalEmails: mb.totalEmails + movedCount,
|
|
||||||
unreadEmails: mb.unreadEmails + unreadDelta,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
return mb;
|
|
||||||
}),
|
|
||||||
}));
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
set({ error: error instanceof Error ? error.message : 'Failed to move emails' });
|
set({ error: error instanceof Error ? error.message : 'Failed to move emails' });
|
||||||
throw error;
|
throw error;
|
||||||
@@ -1928,7 +1969,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
// Batch operations
|
// Batch operations
|
||||||
batchMarkAsRead: async (client, read) => {
|
batchMarkAsRead: async (client, read) => {
|
||||||
const { selectedEmailIds, emails } = get();
|
const { selectedEmailIds, emails } = get();
|
||||||
const mailboxes = resolveActionMailboxes();
|
|
||||||
if (selectedEmailIds.size === 0) return;
|
if (selectedEmailIds.size === 0) return;
|
||||||
|
|
||||||
set({ isLoading: true, error: null });
|
set({ isLoading: true, error: null });
|
||||||
@@ -1965,29 +2005,25 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
: email
|
: email
|
||||||
);
|
);
|
||||||
|
|
||||||
// Update mailbox counters
|
// Update mailbox counters per the email's own account list (#281).
|
||||||
const affectedEmails = emails.filter(e => selectedEmailIds.has(e.id));
|
const affectedEmails = emails.filter(e => selectedEmailIds.has(e.id));
|
||||||
const updatedMailboxes = mailboxes.map(mailbox => {
|
const mailboxPatch = applyBatchMailboxCounterUpdate(get(), affectedEmails, (mailbox, group) => {
|
||||||
let deltaUnread = 0;
|
let deltaUnread = 0;
|
||||||
affectedEmails.forEach(email => {
|
for (const email of group as Email[]) {
|
||||||
if (emailInMailbox(email, mailbox)) {
|
if (emailInMailbox(email, mailbox) && (email.keywords?.$seen ?? false) !== read) {
|
||||||
const wasRead = email.keywords?.$seen;
|
|
||||||
if (wasRead !== read) {
|
|
||||||
deltaUnread += read ? -1 : 1;
|
deltaUnread += read ? -1 : 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
return deltaUnread === 0 ? mailbox : {
|
||||||
|
|
||||||
return {
|
|
||||||
...mailbox,
|
...mailbox,
|
||||||
unreadEmails: Math.max(0, mailbox.unreadEmails + deltaUnread),
|
unreadEmails: Math.max(0, mailbox.unreadEmails + deltaUnread),
|
||||||
unreadThreads: Math.max(0, mailbox.unreadThreads + deltaUnread)
|
unreadThreads: Math.max(0, mailbox.unreadThreads + deltaUnread),
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
set({
|
set({
|
||||||
emails: updatedEmails,
|
emails: updatedEmails,
|
||||||
mailboxes: updatedMailboxes,
|
...mailboxPatch,
|
||||||
selectedEmailIds: new Set(),
|
selectedEmailIds: new Set(),
|
||||||
isLoading: false
|
isLoading: false
|
||||||
});
|
});
|
||||||
@@ -2079,26 +2115,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
if (movedEmailIds.size < emailIdsArray.length) {
|
if (movedEmailIds.size < emailIdsArray.length) {
|
||||||
const deletedEmails = emails.filter(e => movedEmailIds.has(e.id));
|
const deletedEmails = emails.filter(e => movedEmailIds.has(e.id));
|
||||||
const remainingEmails = emails.filter(e => !movedEmailIds.has(e.id));
|
const remainingEmails = emails.filter(e => !movedEmailIds.has(e.id));
|
||||||
const updatedMailboxes = mailboxes.map(mailbox => {
|
const mailboxPatch = applyBatchMailboxCounterUpdate(get(), deletedEmails, applyDeleteCounters);
|
||||||
let deltaTotalEmails = 0;
|
|
||||||
let deltaUnreadEmails = 0;
|
|
||||||
deletedEmails.forEach(email => {
|
|
||||||
if (emailInMailbox(email, mailbox)) {
|
|
||||||
deltaTotalEmails--;
|
|
||||||
if (!email.keywords?.$seen) deltaUnreadEmails--;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
return {
|
|
||||||
...mailbox,
|
|
||||||
totalEmails: Math.max(0, mailbox.totalEmails + deltaTotalEmails),
|
|
||||||
unreadEmails: Math.max(0, mailbox.unreadEmails + deltaUnreadEmails),
|
|
||||||
totalThreads: Math.max(0, mailbox.totalThreads + deltaTotalEmails),
|
|
||||||
unreadThreads: Math.max(0, mailbox.unreadThreads + deltaUnreadEmails),
|
|
||||||
};
|
|
||||||
});
|
|
||||||
set({
|
set({
|
||||||
emails: remainingEmails,
|
emails: remainingEmails,
|
||||||
mailboxes: updatedMailboxes,
|
...mailboxPatch,
|
||||||
selectedEmailIds: new Set(),
|
selectedEmailIds: new Set(),
|
||||||
selectedEmail: null,
|
selectedEmail: null,
|
||||||
isLoading: false,
|
isLoading: false,
|
||||||
@@ -2111,33 +2131,13 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
// Remove deleted emails from local state
|
// Remove deleted emails from local state
|
||||||
const remainingEmails = emails.filter(e => !selectedEmailIds.has(e.id));
|
const remainingEmails = emails.filter(e => !selectedEmailIds.has(e.id));
|
||||||
|
|
||||||
// Update mailbox counters
|
// Update mailbox counters per the email's own account list (#281).
|
||||||
const deletedEmails = emails.filter(e => selectedEmailIds.has(e.id));
|
const deletedEmails = emails.filter(e => selectedEmailIds.has(e.id));
|
||||||
const updatedMailboxes = mailboxes.map(mailbox => {
|
const mailboxPatch = applyBatchMailboxCounterUpdate(get(), deletedEmails, applyDeleteCounters);
|
||||||
let deltaTotalEmails = 0;
|
|
||||||
let deltaUnreadEmails = 0;
|
|
||||||
|
|
||||||
deletedEmails.forEach(email => {
|
|
||||||
if (emailInMailbox(email, mailbox)) {
|
|
||||||
deltaTotalEmails--;
|
|
||||||
if (!email.keywords?.$seen) {
|
|
||||||
deltaUnreadEmails--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
...mailbox,
|
|
||||||
totalEmails: Math.max(0, mailbox.totalEmails + deltaTotalEmails),
|
|
||||||
unreadEmails: Math.max(0, mailbox.unreadEmails + deltaUnreadEmails),
|
|
||||||
totalThreads: Math.max(0, mailbox.totalThreads + deltaTotalEmails),
|
|
||||||
unreadThreads: Math.max(0, mailbox.unreadThreads + deltaUnreadEmails)
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
set({
|
set({
|
||||||
emails: remainingEmails,
|
emails: remainingEmails,
|
||||||
mailboxes: updatedMailboxes,
|
...mailboxPatch,
|
||||||
selectedEmailIds: new Set(),
|
selectedEmailIds: new Set(),
|
||||||
selectedEmail: null,
|
selectedEmail: null,
|
||||||
isLoading: false
|
isLoading: false
|
||||||
@@ -2795,15 +2795,14 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Update mailbox unread counters
|
// Update mailbox unread counters in the thread's own account list (#281).
|
||||||
const affectedEmails = state.emails.filter(e => unreadSet.has(e.id));
|
const affectedEmails = state.emails.filter(e => unreadSet.has(e.id));
|
||||||
const updatedMailboxes = state.mailboxes.map(mailbox => {
|
const mailboxPatch = applyBatchMailboxCounterUpdate(state, affectedEmails, (mailbox, group) => {
|
||||||
let delta = 0;
|
let delta = 0;
|
||||||
for (const email of affectedEmails) {
|
for (const email of group as Email[]) {
|
||||||
if (emailInMailbox(email, mailbox)) delta -= 1;
|
if (emailInMailbox(email, mailbox)) delta -= 1;
|
||||||
}
|
}
|
||||||
if (delta === 0) return mailbox;
|
return delta === 0 ? mailbox : {
|
||||||
return {
|
|
||||||
...mailbox,
|
...mailbox,
|
||||||
unreadEmails: Math.max(0, mailbox.unreadEmails + delta),
|
unreadEmails: Math.max(0, mailbox.unreadEmails + delta),
|
||||||
unreadThreads: Math.max(0, mailbox.unreadThreads + delta),
|
unreadThreads: Math.max(0, mailbox.unreadThreads + delta),
|
||||||
@@ -2813,7 +2812,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
|||||||
return {
|
return {
|
||||||
emails: updatedEmails,
|
emails: updatedEmails,
|
||||||
threadEmailsCache: newCache,
|
threadEmailsCache: newCache,
|
||||||
mailboxes: updatedMailboxes,
|
...mailboxPatch,
|
||||||
selectedEmail: state.selectedEmail && unreadSet.has(state.selectedEmail.id)
|
selectedEmail: state.selectedEmail && unreadSet.has(state.selectedEmail.id)
|
||||||
? { ...state.selectedEmail, keywords: { ...state.selectedEmail.keywords, $seen: true } }
|
? { ...state.selectedEmail, keywords: { ...state.selectedEmail.keywords, $seen: true } }
|
||||||
: state.selectedEmail,
|
: state.selectedEmail,
|
||||||
|
|||||||
Reference in New Issue
Block a user