feat(auth): conversation_view preference, on by default
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
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;
|
||||||
@@ -1195,21 +1195,32 @@ export async function deleteAlias(scope: AdminScope, aliasId: string): Promise<b
|
|||||||
|
|
||||||
// ------------------------------------------------ user preferences & sieve
|
// ------------------------------------------------ user preferences & sieve
|
||||||
|
|
||||||
export async function getUserPreferences(mailboxId: string): Promise<{ signature: string; auto_save_drafts: boolean }> {
|
export async function getUserPreferences(
|
||||||
const row = await queryOne<{ signature: string | null; auto_save_drafts: boolean | null }>(
|
mailboxId: string,
|
||||||
`SELECT signature, auto_save_drafts FROM mailboxes WHERE id = $1`,
|
): 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],
|
[mailboxId],
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
signature: row?.signature ?? "",
|
signature: row?.signature ?? "",
|
||||||
auto_save_drafts: row?.auto_save_drafts ?? true,
|
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 }>(
|
const rows = await query<{ id: string }>(
|
||||||
`UPDATE mailboxes SET signature = $2, auto_save_drafts = $3, updated_at = NOW() WHERE id = $1 RETURNING id`,
|
`UPDATE mailboxes SET signature = $2, auto_save_drafts = $3,
|
||||||
[mailboxId, signature, autoSaveDrafts],
|
conversation_view = COALESCE($4::boolean, conversation_view), updated_at = NOW()
|
||||||
|
WHERE id = $1 RETURNING id`,
|
||||||
|
[mailboxId, signature, autoSaveDrafts, conversationView],
|
||||||
);
|
);
|
||||||
return rows.length === 1;
|
return rows.length === 1;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1218,6 +1218,21 @@ describeDb("moving between the surfaces", () => {
|
|||||||
expect((await send("GET", "/api/v1/user/trusted-senders")).body.senders).toEqual([]);
|
expect((await send("GET", "/api/v1/user/trusted-senders")).body.senders).toEqual([]);
|
||||||
}, 60000);
|
}, 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 () => {
|
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", { address: "not an address" })).status).toBe(400);
|
||||||
expect((await send("PUT", "/api/v1/user/trusted-senders", {})).status).toBe(400);
|
expect((await send("PUT", "/api/v1/user/trusted-senders", {})).status).toBe(400);
|
||||||
|
|||||||
@@ -1224,7 +1224,8 @@ async function userUpdatePreferences(req: IncomingMessage, res: ServerResponse,
|
|||||||
const body = await parseJsonBody(req);
|
const body = await parseJsonBody(req);
|
||||||
const signature = typeof body?.signature === "string" ? body.signature : "";
|
const signature = typeof body?.signature === "string" ? body.signature : "";
|
||||||
const autoSaveDrafts = body?.auto_save_drafts !== false;
|
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 });
|
sendJson(res, 200, { ok: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in new issue
Block a user