feat(auth): conversation_view preference, on by default

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
Lucas RayanandClaude Sonnet 5.5 committed 2026-10-22 16:40:36 -03:00
1 parent 67d0c8aa5a
commit 997dd8848f
5 files changed
+38 -7

No files matched your search

@@ -0,0 +1 @@
ALTER TABLE mailboxes DROP COLUMN IF EXISTS conversation_view;
@@ -0,0 +1,3 @@
-- Whether the webmail lists mail as conversations. On by default: it is what
-- people coming from other mail programs expect, and it is one switch away.
ALTER TABLE mailboxes ADD COLUMN conversation_view BOOLEAN NOT NULL DEFAULT true;
+17 -6
View File
@@ -1195,21 +1195,32 @@ export async function deleteAlias(scope: AdminScope, aliasId: string): Promise<b
// ------------------------------------------------ user preferences & sieve
export async function getUserPreferences(mailboxId: string): Promise<{ signature: string; auto_save_drafts: boolean }> {
const row = await queryOne<{ signature: string | null; auto_save_drafts: boolean | null }>(
`SELECT signature, auto_save_drafts FROM mailboxes WHERE id = $1`,
export async function getUserPreferences(
mailboxId: string,
): Promise<{ signature: string; auto_save_drafts: boolean; conversation_view: boolean }> {
const row = await queryOne<{ signature: string | null; auto_save_drafts: boolean | null; conversation_view: boolean | null }>(
`SELECT signature, auto_save_drafts, conversation_view FROM mailboxes WHERE id = $1`,
[mailboxId],
);
return {
signature: row?.signature ?? "",
auto_save_drafts: row?.auto_save_drafts ?? true,
conversation_view: row?.conversation_view ?? true,
};
}
export async function updateUserPreferences(mailboxId: string, signature: string, autoSaveDrafts: boolean): Promise<boolean> {
/** conversationView is left as it is when the caller does not say. */
export async function updateUserPreferences(
mailboxId: string,
signature: string,
autoSaveDrafts: boolean,
conversationView: boolean | null = null,
): Promise<boolean> {
const rows = await query<{ id: string }>(
`UPDATE mailboxes SET signature = $2, auto_save_drafts = $3, updated_at = NOW() WHERE id = $1 RETURNING id`,
[mailboxId, signature, autoSaveDrafts],
`UPDATE mailboxes SET signature = $2, auto_save_drafts = $3,
conversation_view = COALESCE($4::boolean, conversation_view), updated_at = NOW()
WHERE id = $1 RETURNING id`,
[mailboxId, signature, autoSaveDrafts, conversationView],
);
return rows.length === 1;
}
+15
View File
@@ -1218,6 +1218,21 @@ describeDb("moving between the surfaces", () => {
expect((await send("GET", "/api/v1/user/trusted-senders")).body.senders).toEqual([]);
}, 60000);
it("lists conversations by default and remembers a switch back, without being reset by other saves", async () => {
const prefs = async () =>
(await send("GET", "/api/v1/user/preferences")).body as { conversation_view: boolean; signature: string };
expect((await prefs()).conversation_view).toBe(true);
await send("POST", "/api/v1/user/preferences", { signature: "x", auto_save_drafts: true, conversation_view: false });
expect((await prefs()).conversation_view).toBe(false);
// A save from an older screen that does not mention it must not flip it back.
await send("POST", "/api/v1/user/preferences", { signature: "y", auto_save_drafts: true });
const after = await prefs();
expect(after.conversation_view).toBe(false);
expect(after.signature).toBe("y");
}, 60000);
it("refuses something that is not an address", async () => {
expect((await send("PUT", "/api/v1/user/trusted-senders", { address: "not an address" })).status).toBe(400);
expect((await send("PUT", "/api/v1/user/trusted-senders", {})).status).toBe(400);
+2 -1
View File
@@ -1224,7 +1224,8 @@ async function userUpdatePreferences(req: IncomingMessage, res: ServerResponse,
const body = await parseJsonBody(req);
const signature = typeof body?.signature === "string" ? body.signature : "";
const autoSaveDrafts = body?.auto_save_drafts !== false;
await repo.updateUserPreferences(session.sub, signature, autoSaveDrafts);
const conversationView = typeof body?.conversation_view === "boolean" ? body.conversation_view : null;
await repo.updateUserPreferences(session.sub, signature, autoSaveDrafts, conversationView);
sendJson(res, 200, { ok: true });
}