fix: reuse a group account's year and month archive folders instead of creating them again or in the own account

This commit is contained in:
Linus Rath
2026-10-26 16:14:19 +01:00
parent 4a9284aa48
commit 272fc5e1f4
2 changed files with 74 additions and 8 deletions
@@ -195,3 +195,63 @@ describe('the open message of a group account in a tag view', () => {
expect(moveToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:b', 'login-a/ce:eaaaaab');
});
});
describe('archiving a group message by month', () => {
// The group's Archive `k` already holds 2026/10, as the list names them:
// store ids and parents namespaced `ce:...`, the server's ids bare.
const year = { ...mailbox('y1', undefined, 'ce', true), name: '2026', parentId: 'ce:k' };
const month = { ...mailbox('m1', undefined, 'ce', true), name: '10', parentId: 'ce:y1' };
const withMonth = [...folders, year, month] as Mailbox[];
beforeEach(() => {
useSettingsStore.setState({ archiveMode: 'month' } as never);
client.createMailbox = vi.fn();
});
it('reuses the year and month folders in a tag view', async () => {
useEmailStore.setState({ mailboxes: withMonth } as never);
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(moveThreadToMailbox).toHaveBeenCalled());
expect(client.createMailbox).not.toHaveBeenCalled();
expect(moveThreadToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:m1', 'login-a/ce:eaaaaab');
});
it('reuses them with the group\'s Inbox open', async () => {
const unstamped = { ...groupMessage, sourceClientAccountId: undefined, sourceAccountId: undefined } as Email;
useEmailStore.setState({ mailboxes: withMonth, selectedMailbox: 'ce:a', selectedKeyword: null, emails: [unstamped], selectedEmail: unstamped } as never);
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(moveThreadToMailbox).toHaveBeenCalled());
expect(client.createMailbox).not.toHaveBeenCalled();
expect(moveThreadToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:m1', 'eaaaaab');
});
// The own account has a folder with the server id of the group's Archive,
// with 2026/10 below it (live on 0.16: a's "Shared" folder is `h`, and so
// was the group's Archive).
it('does not take the own account\'s namesake folders', async () => {
const unstamped = { ...groupMessage, sourceClientAccountId: undefined, sourceAccountId: undefined } as Email;
const ownNamesakes = [
{ ...mailbox('k', undefined, 'cf'), name: 'Shared' },
{ ...mailbox('oy', undefined, 'cf'), name: '2026', parentId: 'k' },
{ ...mailbox('om', undefined, 'cf'), name: '10', parentId: 'oy' },
];
useEmailStore.setState({ mailboxes: [...ownNamesakes, ...withMonth], selectedMailbox: 'ce:a', selectedKeyword: null, emails: [unstamped], selectedEmail: unstamped } as never);
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(moveThreadToMailbox).toHaveBeenCalled());
expect(moveThreadToMailbox).toHaveBeenCalledWith(client, 'eaaaaab', 'ce:m1', 'eaaaaab');
});
it('creates a missing year folder in the group account with the group\'s Inbox open', async () => {
const unstamped = { ...groupMessage, sourceClientAccountId: undefined, sourceAccountId: undefined } as Email;
useSettingsStore.setState({ archiveMode: 'year' } as never);
(client.createMailbox as ReturnType<typeof vi.fn>).mockResolvedValue({ ...mailbox('y2', undefined, 'ce', true), id: 'y2', name: '2026' });
useEmailStore.setState({ selectedMailbox: 'ce:a', selectedKeyword: null, emails: [unstamped], selectedEmail: unstamped } as never);
render(<MailApp />);
fireEvent.click(await screen.findByText('stub archive', {}, { timeout: 5000 }));
await waitFor(() => expect(client.createMailbox).toHaveBeenCalled());
expect(client.createMailbox).toHaveBeenCalledWith('2026', 'k', 'ce', undefined);
});
});
+14 -8
View File
@@ -2948,12 +2948,20 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
const year = emailDate.getFullYear().toString();
const month = (emailDate.getMonth() + 1).toString().padStart(2, '0');
const archiveId = archiveMailbox.originalId || archiveMailbox.id;
// By the server's ids: the list names a shared account's folders and
// their parents `account:id`, so a group's year folder was never
// found again. The folders go in the Archive's own account, and only
// that account's are looked at: the own folders have server ids too.
const parentOf = (m: Mailbox) => (m.parentId && m.accountId && m.parentId.startsWith(`${m.accountId}:`)
? m.parentId.slice(m.accountId.length + 1)
: m.parentId);
const folderAccountId = archiveMailbox.accountId ?? archiveAccountId;
const isFolder = (m: Mailbox, name: string, parentId: string) =>
m.name === name && parentOf(m) === parentId && (!folderAccountId || m.accountId === folderAccountId);
let yearMailbox = currentMailboxes.find(
m => m.name === year && m.parentId === archiveId
) ?? null;
let yearMailbox = currentMailboxes.find(m => isFolder(m, year, archiveId)) ?? null;
if (!yearMailbox) {
yearMailbox = await createMailboxWithHooks(archiveClient, year, archiveId, archiveAccountId);
yearMailbox = await createMailboxWithHooks(archiveClient, year, archiveId, folderAccountId);
if (!yearMailbox) return; // a plugin refused the folder
await refreshMailboxes();
}
@@ -2963,11 +2971,9 @@ export function MailApp({ linkSegments: routeSegments }: MailAppProps = {}) {
} else {
const yearId = yearMailbox.originalId || yearMailbox.id;
const afterYear = readMailboxes();
let monthMailbox = afterYear.find(
m => m.name === month && m.parentId === yearId
) ?? null;
let monthMailbox = afterYear.find(m => isFolder(m, month, yearId)) ?? null;
if (!monthMailbox) {
monthMailbox = await createMailboxWithHooks(archiveClient, month, yearId, archiveAccountId);
monthMailbox = await createMailboxWithHooks(archiveClient, month, yearId, folderAccountId);
if (!monthMailbox) return; // a plugin refused the folder
await refreshMailboxes();
}