mail list redesign, folder actions, empty-folder, unread badges & empty-state art
Frontend:
- Redesign the mail list rows (avatar + sender + subject/snippet + right rail),
even-spaced attachment/pin/archive/trash icons, hover-reveal actions, pink
always-on pin, monocolor paperclip, row gaps, wider sender-subject gap.
- Sidebar folder badges: Inbox unread + Drafts counts (live via WS + mutations).
- Trash/Spam/Archive: hover Restore icon (replaces Archive), permanent delete in
Trash; "Empty trash" / "Empty spam" buttons on the sort bar.
- Reading pane empty state: email illustration above an enlarged
"Select a conversation".
- Search field clear (X) button; mark-read after a 4s dwell instead of instantly.
Backend:
- GET /api/mailboxes/counts (inbox unread + drafts) for the sidebar badges.
- POST /api/folders/{special}/empty + empty_folder_job to permanently clear a
Trash/Spam/Archive folder (server EXPUNGE + DB rows + thread cleanup).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01U72FZiF1W1pb8XxyWBTREA
This commit is contained in:
@@ -298,6 +298,22 @@ async def test_thread_detail_hides_trashed_message_unless_viewing_trash(api: Api
|
||||
assert trow["message_count"] == 1
|
||||
|
||||
|
||||
async def test_folder_counts(api: ApiCtx) -> None:
|
||||
assert api.session is not None
|
||||
# All three seeded inbox messages are unread; no drafts yet.
|
||||
resp = await api.client.get("/api/mailboxes/counts")
|
||||
assert resp.status_code == 200
|
||||
assert resp.json() == {"inbox_unread": 3, "drafts": 0}
|
||||
|
||||
# Marking one message \Seen lowers the unread count.
|
||||
msg = await api.session.get(Message, api.message_ids[0])
|
||||
assert msg is not None
|
||||
msg.flags = ["\\Seen"]
|
||||
await api.session.commit()
|
||||
body = (await api.client.get("/api/mailboxes/counts")).json()
|
||||
assert body["inbox_unread"] == 2
|
||||
|
||||
|
||||
async def test_message_html_is_sanitized(api: ApiCtx) -> None:
|
||||
# the lunch message has HTML with a script
|
||||
resp = await api.client.get("/api/threads")
|
||||
|
||||
@@ -245,6 +245,18 @@ async def test_delete_thread_enqueues_permanent_delete(ctx: Ctx) -> None:
|
||||
assert ctx.enqueued == [("delete_thread_job", str(ctx.thread_id))]
|
||||
|
||||
|
||||
async def test_empty_folder_enqueues_job(ctx: Ctx) -> None:
|
||||
resp = await ctx.client.post("/api/folders/trash/empty", headers=_hdr())
|
||||
assert resp.status_code == 200
|
||||
assert ctx.enqueued == [("empty_folder_job", str(ctx.user_id), "trash")]
|
||||
|
||||
|
||||
async def test_empty_folder_rejects_non_removable_folder(ctx: Ctx) -> None:
|
||||
resp = await ctx.client.post("/api/folders/inbox/empty", headers=_hdr())
|
||||
assert resp.status_code == 400
|
||||
assert ctx.enqueued == []
|
||||
|
||||
|
||||
async def test_ignores_unknown_flags(ctx: Ctx) -> None:
|
||||
mid = ctx.message_ids[0]
|
||||
resp = await ctx.client.post(
|
||||
|
||||
Reference in New Issue
Block a user