fix(mailbox): de-duplicate folder names in the drawer #54

Merged
JMR-dev merged 2 commits from fix-drawer-duplicate-folders into main 2026-07-01 20:24:57 +00:00
JMR-dev commented 2026-07-01 20:01:37 +00:00 (Migrated from github.com)

Problem

On a Gmail account the drawer showed Drafts, Archive, and Spam more than once. The cause was a display-layer collapse: folderDisplayLabel rendered every standard-role folder with a generic friendly name ("Drafts", "Archive", …) and discarded the distinguishing server name. When an account has both the provider's built-in special folder and a same-named user folder, both collapsed to one entry:

  • [Gmail]/Drafts (\Drafts) and a label Drafts → both "Drafts"
  • [Gmail]/All Mail (→ ARCHIVE) and a label Archive → both "Archive"
  • [Gmail]/Spam (\Junk) and a label Spam → both "Spam"

These are genuinely distinct folders, so the fix relabels rather than hides.

Fix

A provider-agnostic de-duplication rule: when 2+ folders would render the same label,

  • the provider's built-in special folder (identified by IMAP SPECIAL-USE flags, RFC 6154) gets the provider name appended → Archive - Gmail;
  • a nested user folder gets its parent location → Reports (Work);
  • a top-level user folder keeps its plain name.

It only triggers on a real collision, so stock accounts are unchanged. The provider suffix is a recognized brand (Gmail/Yahoo Mail/iCloud Mail/Outlook), else the email domain.

Implementation

  • Room v11 → v12: persist a specialUse flag on the folders cache (ALTER TABLE … ADD COLUMN specialUse INTEGER NOT NULL DEFAULT 0, matching @ColumnInfo(defaultValue = "0"); schema 12.json committed). Existing rows migrate to false and are backfilled on the next folder refresh (which the drawer already triggers on open).
  • FolderRole.isServerSpecial(attributes) classifies the SPECIAL-USE set; Folder.specialUse carries it; MailProvider.forImapHost(host) resolves the brand.
  • New Compose-free, unit-tested FolderLabels.kt (providerLabel + pure resolveDrawerLabels), wired into FolderDrawer.

Tests

  • New JVM FolderLabelsTest — all three reported cases, nested-folder parents, no-op path, provider-label resolution, and a uniqueness safety net.
  • FolderRoleTest.isServerSpecial cases; a Gmail duplicate case in FolderDrawerTest; specialUse round-trip in LibreMailDatabaseTest.
  • Locally green: assembleDebug + testDebugUnitTest + lintDebug + compileDebugAndroidTestKotlin.

🤖 Generated with Claude Code

## Problem On a Gmail account the drawer showed **Drafts, Archive, and Spam more than once**. The cause was a display-layer collapse: `folderDisplayLabel` rendered every standard-role folder with a generic friendly name ("Drafts", "Archive", …) and discarded the distinguishing server name. When an account has both the provider's built-in special folder **and** a same-named user folder, both collapsed to one entry: - `[Gmail]/Drafts` (`\Drafts`) **and** a label `Drafts` → both "Drafts" - `[Gmail]/All Mail` (→ `ARCHIVE`) **and** a label `Archive` → both "Archive" - `[Gmail]/Spam` (`\Junk`) **and** a label `Spam` → both "Spam" These are genuinely distinct folders, so the fix relabels rather than hides. ## Fix A provider-agnostic de-duplication rule: when 2+ folders would render the same label, - the provider's **built-in special folder** (identified by IMAP SPECIAL-USE flags, RFC 6154) gets the provider name appended → `Archive - Gmail`; - a **nested** user folder gets its parent location → `Reports (Work)`; - a **top-level** user folder keeps its plain name. It only triggers on a real collision, so stock accounts are unchanged. The provider suffix is a recognized brand (`Gmail`/`Yahoo Mail`/`iCloud Mail`/`Outlook`), else the email domain. ## Implementation - **Room v11 → v12:** persist a `specialUse` flag on the `folders` cache (`ALTER TABLE … ADD COLUMN specialUse INTEGER NOT NULL DEFAULT 0`, matching `@ColumnInfo(defaultValue = "0")`; schema `12.json` committed). Existing rows migrate to `false` and are backfilled on the next folder refresh (which the drawer already triggers on open). - `FolderRole.isServerSpecial(attributes)` classifies the SPECIAL-USE set; `Folder.specialUse` carries it; `MailProvider.forImapHost(host)` resolves the brand. - New Compose-free, unit-tested `FolderLabels.kt` (`providerLabel` + pure `resolveDrawerLabels`), wired into `FolderDrawer`. ## Tests - New JVM `FolderLabelsTest` — all three reported cases, nested-folder parents, no-op path, provider-label resolution, and a uniqueness safety net. - `FolderRoleTest.isServerSpecial` cases; a Gmail duplicate case in `FolderDrawerTest`; `specialUse` round-trip in `LibreMailDatabaseTest`. - Locally green: `assembleDebug` + `testDebugUnitTest` + `lintDebug` + `compileDebugAndroidTestKotlin`. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
Sign in to join this conversation.