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),
|
||||
toggleStar: vi.fn().mockResolvedValue(undefined),
|
||||
moveEmail: vi.fn().mockResolvedValue(undefined),
|
||||
batchMarkAsRead: vi.fn().mockResolvedValue(undefined),
|
||||
} 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
|
||||
});
|
||||
|
||||
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 () => {
|
||||
await useEmailStore.getState().markAsRead(activeClient, 'email-shared', true);
|
||||
// Reached via account-a's client (the active one), targeting the owner account.
|
||||
|
||||
+153
-154
@@ -550,6 +550,60 @@ function applyMailboxCounterUpdate(
|
||||
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
|
||||
// falls back to name matching ("trash" / "deleted") so users with custom or
|
||||
// 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.
|
||||
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) => {
|
||||
let updatedMailboxes = state.mailboxes;
|
||||
|
||||
// Update counters for source mailbox (email leaving)
|
||||
if (email.mailboxIds) {
|
||||
updatedMailboxes = state.mailboxes.map(mailbox => {
|
||||
if (emailInMailbox(email, mailbox)) {
|
||||
return {
|
||||
...mailbox,
|
||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
||||
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
||||
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads
|
||||
};
|
||||
}
|
||||
// Update trash mailbox counters (email arriving)
|
||||
if (mailbox.id === trashMailbox.id) {
|
||||
return {
|
||||
...mailbox,
|
||||
totalEmails: mailbox.totalEmails + 1,
|
||||
unreadEmails: arrivesUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
||||
totalThreads: mailbox.totalThreads + 1,
|
||||
unreadThreads: arrivesUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads
|
||||
};
|
||||
}
|
||||
return mailbox;
|
||||
});
|
||||
}
|
||||
const mailboxPatch = email.mailboxIds
|
||||
? applyMailboxCounterUpdate(state, email, (mailbox) => {
|
||||
if (emailInMailbox(email, mailbox)) {
|
||||
return {
|
||||
...mailbox,
|
||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
||||
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
||||
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads,
|
||||
};
|
||||
}
|
||||
if (mailbox.id === trashMailbox.id) {
|
||||
return {
|
||||
...mailbox,
|
||||
totalEmails: mailbox.totalEmails + 1,
|
||||
unreadEmails: arrivesUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
||||
totalThreads: mailbox.totalThreads + 1,
|
||||
unreadThreads: arrivesUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads,
|
||||
};
|
||||
}
|
||||
return mailbox;
|
||||
})
|
||||
: {};
|
||||
|
||||
return {
|
||||
emails: state.emails.filter(e => e.id !== emailId),
|
||||
selectedEmail: getNextSelectedEmail(state, emailId),
|
||||
mailboxes: updatedMailboxes
|
||||
...mailboxPatch,
|
||||
};
|
||||
});
|
||||
return;
|
||||
@@ -1264,42 +1316,26 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// Permanent delete
|
||||
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) => {
|
||||
let updatedMailboxes = state.mailboxes;
|
||||
|
||||
// If the email was unread, decrement the unread counters
|
||||
if (isUnread && email.mailboxIds) {
|
||||
updatedMailboxes = state.mailboxes.map(mailbox => {
|
||||
if (emailInMailbox(email, mailbox)) {
|
||||
return {
|
||||
...mailbox,
|
||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||
unreadEmails: Math.max(0, mailbox.unreadEmails - 1),
|
||||
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
||||
unreadThreads: Math.max(0, mailbox.unreadThreads - 1)
|
||||
};
|
||||
}
|
||||
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;
|
||||
});
|
||||
}
|
||||
const mailboxPatch = email.mailboxIds
|
||||
? applyMailboxCounterUpdate(state, email, (mailbox) =>
|
||||
emailInMailbox(email, mailbox)
|
||||
? {
|
||||
...mailbox,
|
||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
||||
totalThreads: Math.max(0, mailbox.totalThreads - 1),
|
||||
unreadThreads: isUnread ? Math.max(0, mailbox.unreadThreads - 1) : mailbox.unreadThreads,
|
||||
}
|
||||
: mailbox)
|
||||
: {};
|
||||
|
||||
return {
|
||||
emails: state.emails.filter(e => e.id !== emailId),
|
||||
selectedEmail: getNextSelectedEmail(state, emailId),
|
||||
mailboxes: updatedMailboxes
|
||||
...mailboxPatch,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1415,7 +1451,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
if (!email) return;
|
||||
|
||||
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
|
||||
// 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);
|
||||
|
||||
set((state) => {
|
||||
const updatedMailboxes = state.mailboxes.map(mailbox => {
|
||||
if (currentMailboxIds.includes(mailbox.id) || (!mailbox.isShared && mailbox.originalId ? currentMailboxIds.includes(mailbox.originalId) : false)) {
|
||||
// Counter changes go to the email's own account list; source and
|
||||
// destination live in the same account. (#281)
|
||||
const mailboxPatch = applyMailboxCounterUpdate(state, email, (mailbox) => {
|
||||
if (emailInMailbox(email, mailbox)) {
|
||||
return {
|
||||
...mailbox,
|
||||
totalEmails: Math.max(0, mailbox.totalEmails - 1),
|
||||
unreadEmails: isUnread ? Math.max(0, mailbox.unreadEmails - 1) : mailbox.unreadEmails,
|
||||
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) {
|
||||
@@ -1444,7 +1481,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
totalEmails: mailbox.totalEmails + 1,
|
||||
unreadEmails: isUnread ? mailbox.unreadEmails + 1 : mailbox.unreadEmails,
|
||||
totalThreads: mailbox.totalThreads + 1,
|
||||
unreadThreads: isUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads
|
||||
unreadThreads: isUnread ? mailbox.unreadThreads + 1 : mailbox.unreadThreads,
|
||||
};
|
||||
}
|
||||
return mailbox;
|
||||
@@ -1453,7 +1490,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
return {
|
||||
emails: state.emails.filter(e => e.id !== emailId),
|
||||
selectedEmail: getNextSelectedEmail(state, emailId),
|
||||
mailboxes: updatedMailboxes
|
||||
...mailboxPatch,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
@@ -1506,39 +1543,43 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
|
||||
// Adjust counters and drop moved emails from the current view.
|
||||
let unreadDelta = 0;
|
||||
const sourceMailboxIds = new Set<string>();
|
||||
for (const e of affected) {
|
||||
if (!e.keywords?.$seen) unreadDelta += 1;
|
||||
if (e.mailboxIds) for (const mid of Object.keys(e.mailboxIds)) sourceMailboxIds.add(mid);
|
||||
}
|
||||
const movedCount = affected.length;
|
||||
|
||||
set((state) => ({
|
||||
emails: state.emails.filter(e => !idSet.has(e.id)),
|
||||
selectedEmail: state.selectedEmail && idSet.has(state.selectedEmail.id) ? null : state.selectedEmail,
|
||||
selectedEmailIds: (() => {
|
||||
const next = new Set(state.selectedEmailIds);
|
||||
for (const id of idSet) next.delete(id);
|
||||
return next;
|
||||
})(),
|
||||
mailboxes: state.mailboxes.map(mb => {
|
||||
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),
|
||||
};
|
||||
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--;
|
||||
}
|
||||
}
|
||||
if (mb.id === destinationMailboxId) {
|
||||
return {
|
||||
...mb,
|
||||
totalEmails: mb.totalEmails + movedCount,
|
||||
unreadEmails: mb.unreadEmails + unreadDelta,
|
||||
};
|
||||
}
|
||||
return mb;
|
||||
}),
|
||||
}));
|
||||
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)),
|
||||
selectedEmail: state.selectedEmail && idSet.has(state.selectedEmail.id) ? null : state.selectedEmail,
|
||||
selectedEmailIds: (() => {
|
||||
const next = new Set(state.selectedEmailIds);
|
||||
for (const id of idSet) next.delete(id);
|
||||
return next;
|
||||
})(),
|
||||
...patch,
|
||||
};
|
||||
});
|
||||
} catch (error) {
|
||||
set({ error: error instanceof Error ? error.message : 'Failed to move emails' });
|
||||
throw error;
|
||||
@@ -1928,7 +1969,6 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// Batch operations
|
||||
batchMarkAsRead: async (client, read) => {
|
||||
const { selectedEmailIds, emails } = get();
|
||||
const mailboxes = resolveActionMailboxes();
|
||||
if (selectedEmailIds.size === 0) return;
|
||||
|
||||
set({ isLoading: true, error: null });
|
||||
@@ -1965,29 +2005,25 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
: 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 updatedMailboxes = mailboxes.map(mailbox => {
|
||||
const mailboxPatch = applyBatchMailboxCounterUpdate(get(), affectedEmails, (mailbox, group) => {
|
||||
let deltaUnread = 0;
|
||||
affectedEmails.forEach(email => {
|
||||
if (emailInMailbox(email, mailbox)) {
|
||||
const wasRead = email.keywords?.$seen;
|
||||
if (wasRead !== read) {
|
||||
deltaUnread += read ? -1 : 1;
|
||||
}
|
||||
for (const email of group as Email[]) {
|
||||
if (emailInMailbox(email, mailbox) && (email.keywords?.$seen ?? false) !== read) {
|
||||
deltaUnread += read ? -1 : 1;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
}
|
||||
return deltaUnread === 0 ? mailbox : {
|
||||
...mailbox,
|
||||
unreadEmails: Math.max(0, mailbox.unreadEmails + deltaUnread),
|
||||
unreadThreads: Math.max(0, mailbox.unreadThreads + deltaUnread)
|
||||
unreadThreads: Math.max(0, mailbox.unreadThreads + deltaUnread),
|
||||
};
|
||||
});
|
||||
|
||||
set({
|
||||
emails: updatedEmails,
|
||||
mailboxes: updatedMailboxes,
|
||||
...mailboxPatch,
|
||||
selectedEmailIds: new Set(),
|
||||
isLoading: false
|
||||
});
|
||||
@@ -2079,26 +2115,10 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
if (movedEmailIds.size < emailIdsArray.length) {
|
||||
const deletedEmails = emails.filter(e => movedEmailIds.has(e.id));
|
||||
const remainingEmails = emails.filter(e => !movedEmailIds.has(e.id));
|
||||
const updatedMailboxes = mailboxes.map(mailbox => {
|
||||
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),
|
||||
};
|
||||
});
|
||||
const mailboxPatch = applyBatchMailboxCounterUpdate(get(), deletedEmails, applyDeleteCounters);
|
||||
set({
|
||||
emails: remainingEmails,
|
||||
mailboxes: updatedMailboxes,
|
||||
...mailboxPatch,
|
||||
selectedEmailIds: new Set(),
|
||||
selectedEmail: null,
|
||||
isLoading: false,
|
||||
@@ -2111,33 +2131,13 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
// Remove deleted emails from local state
|
||||
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 updatedMailboxes = mailboxes.map(mailbox => {
|
||||
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)
|
||||
};
|
||||
});
|
||||
const mailboxPatch = applyBatchMailboxCounterUpdate(get(), deletedEmails, applyDeleteCounters);
|
||||
|
||||
set({
|
||||
emails: remainingEmails,
|
||||
mailboxes: updatedMailboxes,
|
||||
...mailboxPatch,
|
||||
selectedEmailIds: new Set(),
|
||||
selectedEmail: null,
|
||||
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 updatedMailboxes = state.mailboxes.map(mailbox => {
|
||||
const mailboxPatch = applyBatchMailboxCounterUpdate(state, affectedEmails, (mailbox, group) => {
|
||||
let delta = 0;
|
||||
for (const email of affectedEmails) {
|
||||
for (const email of group as Email[]) {
|
||||
if (emailInMailbox(email, mailbox)) delta -= 1;
|
||||
}
|
||||
if (delta === 0) return mailbox;
|
||||
return {
|
||||
return delta === 0 ? mailbox : {
|
||||
...mailbox,
|
||||
unreadEmails: Math.max(0, mailbox.unreadEmails + delta),
|
||||
unreadThreads: Math.max(0, mailbox.unreadThreads + delta),
|
||||
@@ -2813,7 +2812,7 @@ export const useEmailStore = create<EmailStore>((set, get) => ({
|
||||
return {
|
||||
emails: updatedEmails,
|
||||
threadEmailsCache: newCache,
|
||||
mailboxes: updatedMailboxes,
|
||||
...mailboxPatch,
|
||||
selectedEmail: state.selectedEmail && unreadSet.has(state.selectedEmail.id)
|
||||
? { ...state.selectedEmail, keywords: { ...state.selectedEmail.keywords, $seen: true } }
|
||||
: state.selectedEmail,
|
||||
|
||||
Reference in New Issue
Block a user